diff --git a/kmbox/mbox.cpp b/kmbox/mbox.cpp index 625a4012b..4e91bb705 100644 --- a/kmbox/mbox.cpp +++ b/kmbox/mbox.cpp @@ -1,726 +1,713 @@ /* Copyright (c) 1996-1998 Stefan Taferner Copyright (c) 2009 Bertjan Broeksema This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. NOTE: Most of the code inside here is an slightly adjusted version of kdepim/kmail/kmfoldermbox.cpp. This is why I added a copyright line for Stefan Taferner. Bertjan Broeksema, april 2009 */ #include "mbox.h" #include #include #include #include #include #include #include #include #include #include class MBox::Private { public: Private() : mInitialMboxFileSize( 0 ), mLock( 0 ) {} ~Private() { if ( mMboxFile.isOpen() ) mMboxFile.close(); if ( mLock && mLock->isLocked() ) mLock->unlock(); delete mLock; mLock = 0; } void close() { if ( mMboxFile.isOpen() ) mMboxFile.close(); mFileLocked = false; } QByteArray mAppendedEntries; QList mEntries; bool mFileLocked; quint64 mInitialMboxFileSize; KLockFile *mLock; LockType mLockType; QFile mMboxFile; QString mLockFileName; bool mReadOnly; }; static QString sMBoxSeperatorRegExp( "^From .*[0-9][0-9]:[0-9][0-9]" ); /// private static methods. QByteArray quoteAndEncode(const QString &str) { return QFile::encodeName(KShell::quoteArg(str)); } /// public methods. MBox::MBox() : d(new Private()) { // Set some sane defaults d->mFileLocked = false; d->mLockType = None; // } MBox::~MBox() { if ( d->mFileLocked ) unlock(); d->close(); delete d; } qint64 MBox::appendEntry( const MessagePtr &entry ) { if ( d->mMboxFile.fileName().isEmpty() ) return -1; // It doesn't make sense to add entries when we don't have an reference file. const QByteArray rawEntry = escapeFrom( entry->encodedContent() ); if ( rawEntry.size() <= 0 ) { kDebug() << "Message added to folder `" << d->mMboxFile.fileName() << "' contains no data. Ignoring it."; return -1; } int nextOffset = d->mAppendedEntries.size(); // Offset of the appended message // Make sure the byte array is large enough to check for an end character. // Then check if the required newlines are there. if ( nextOffset < 1 && d->mMboxFile.size() > 0 ) { // Empty, add one empty line d->mAppendedEntries.append( "\n"); ++nextOffset; } else if ( nextOffset == 1 && d->mAppendedEntries.at( 0 ) != '\n' ) { // This should actually not happen, but catch it anyway. if (d->mMboxFile.size() < 0 ) { d->mAppendedEntries.append( "\n"); ++nextOffset; } } else if (nextOffset >= 2) { if ( d->mAppendedEntries.at( nextOffset - 1 ) != '\n' ) { if ( d->mAppendedEntries.at( nextOffset ) != '\n' ) { d->mAppendedEntries.append( "\n\n" ); nextOffset += 2; } else { d->mAppendedEntries.append( "\n" ); ++nextOffset; } } } d->mAppendedEntries.append( mboxMessageSeparator( rawEntry ) ); d->mAppendedEntries.append( rawEntry ); if ( rawEntry[rawEntry.size() - 1] != '\n' ) { d->mAppendedEntries.append( "\n\n" ); } else { d->mAppendedEntries.append( "\n" ); } MsgInfo info; info.first = d->mInitialMboxFileSize + nextOffset; info.second = rawEntry.size(); d->mEntries << info; return d->mInitialMboxFileSize + nextOffset; } QList MBox::entryList(const QSet &deletedItems) const { QList result; foreach ( const MsgInfo &info, d->mEntries ) { if ( !deletedItems.contains( info.first ) ) result << info; } return result; } bool MBox::load( const QString &fileName ) { if ( d->mFileLocked ) return false; d->mMboxFile.setFileName( KUrl(fileName).path() ); if ( !d->mMboxFile.exists() && !d->mMboxFile.open( QIODevice::WriteOnly ) ) return false; if ( ! lock() ) return false; d->mAppendedEntries.clear(); d->mEntries.clear(); QRegExp regexp( sMBoxSeperatorRegExp ); QByteArray line; QByteArray prevSeparator; quint64 offs = 0; // The offset of the next message to read. while ( !d->mMboxFile.atEnd() ) { quint64 pos = d->mMboxFile.pos(); line = d->mMboxFile.readLine(); if ( regexp.indexIn(line) >= 0 || d->mMboxFile.atEnd() ) { // Found the separator or at end of file, the message starts at offs quint64 msgSize = pos - offs; if( pos > 0 ) { // This is not the separator of the first mail in the file. If pos == 0 // than we matched the separator of the first mail in the file. MsgInfo info; info.first = offs; // There is always a blank line and a seperator line between two emails. // Sometimes there are two '\n' characters added to the email (i.e. when // the mail self did not end with a '\n' char) and sometimes only one to // achieve this. When reading the file it is not possible to see which // was the case. if ( d->mMboxFile.atEnd() ) info.second = msgSize; // We use readLine so there's no additional '\n' else info.second = msgSize - 1; // Don't add the seperator size and the newline up to the message size. info.second -= prevSeparator.size() + 1; d->mEntries << info; } if ( regexp.indexIn(line) >= 0 ) prevSeparator = line; offs += msgSize; // Mark the beginning of the next message. } } return unlock(); // FIXME: What if unlock fails? } bool MBox::lock() { if ( d->mMboxFile.fileName().isEmpty() ) return false; // We cannot lock if there is no file loaded. d->mFileLocked = false; QStringList args; int rc = 0; switch(d->mLockType) { case KDELockFile: /* FIXME: Don't use the mbox file itself as lock file. if ((rc = d->mLock.lock(KLockFile::ForceFlag))) { kDebug() << "KLockFile lock failed: (" << rc << ") switching to read only mode"; d->mReadOnly = true; } */ break; // We only need to lock the file using the QReadWriteLock case ProcmailLockfile: args << "-l20" << "-r5"; if (!d->mLockFileName.isEmpty()) args << quoteAndEncode(d->mLockFileName); else args << quoteAndEncode(d->mMboxFile.fileName() + ".lock"); rc = QProcess::execute("lockfile", args); if(rc != 0) { kDebug() << "lockfile -l20 -r5 " << d->mMboxFile.fileName() << ": Failed ("<< rc << ") switching to read only mode"; d->mReadOnly = true; // In case the MBox object was created read/write we // set it to read only when locking failed. } else { d->mFileLocked = true; } break; case MuttDotlock: args << quoteAndEncode(d->mMboxFile.fileName()); rc = QProcess::execute("mutt_dotlock", args); if(rc != 0) { kDebug() << "mutt_dotlock " << d->mMboxFile.fileName() << ": Failed (" << rc << ") switching to read only mode"; d->mReadOnly = true; // In case the MBox object was created read/write we // set it to read only when locking failed. } else { d->mFileLocked = true; } break; case MuttDotlockPrivileged: args << "-p" << quoteAndEncode(d->mMboxFile.fileName()); rc = QProcess::execute("mutt_dotlock", args); if(rc != 0) { kDebug() << "mutt_dotlock -p " << d->mMboxFile.fileName() << ":" << ": Failed (" << rc << ") switching to read only mode"; d->mReadOnly = true; } else { d->mFileLocked = true; } break; case None: d->mFileLocked = true; break; default: break; } if ( d->mFileLocked ) { if ( !open() ) { const bool unlocked = unlock(); Q_ASSERT( unlocked ); // If this fails we're in trouble. Q_UNUSED( unlocked ); } } return d->mFileLocked; } static bool lessThanByOffset( const MsgInfo &left, const MsgInfo &right ) { return left.first < right.first; } bool MBox::purge( const QSet &deletedItems ) { if ( d->mMboxFile.fileName().isEmpty() ) return false; // No file loaded yet. if ( deletedItems.isEmpty() ) return true; // Nothing to do. if ( !lock() ) return false; foreach ( quint64 offset, deletedItems ) { d->mMboxFile.seek( offset ); QByteArray line = d->mMboxFile.readLine(); QRegExp regexp( sMBoxSeperatorRegExp ); if ( regexp.indexIn(line) < 0 ) { + qDebug() << "Found invalid seperator at:" << offset; unlock(); return false; // The file is messed up or the index is incorrect. } } + // All entries are deleted, so just resize the file to a size of 0. + if ( deletedItems.size() == d->mEntries.size() ) { + d->mEntries.clear(); + d->mMboxFile.resize( 0 ); + kDebug() << "Purge comleted successfully, unlocking the file."; + return unlock(); + } + qSort( d->mEntries.begin(), d->mEntries.end(), lessThanByOffset ); quint64 writeOffset = 0; + bool writeOffSetInitialized = false; QList resultingEntryList; + quint64 origFileSize = d->mMboxFile.size(); + QListIterator i( d->mEntries ); while ( i.hasNext() ) { MsgInfo entry = i.next(); - if ( deletedItems.contains( entry.first ) ) { - // This entry must get removed from the file. + if ( deletedItems.contains( entry.first ) && !writeOffSetInitialized ) { writeOffset = entry.first; - + writeOffSetInitialized = true; + } else if ( writeOffset < entry.first && !deletedItems.contains( entry.first ) ) { + // The current message doesn't have to be deleted, but must be moved. + // First determine the size of the entry that must be moved. + quint64 entrySize = 0; if ( i.hasNext() ) { - // One ore more entries after this one, find the first that should be - // kept, or find eof if all following entries must be deleted. - MsgInfo entryToWrite; - bool nextEntryFound = false; - while ( i.hasNext() && !nextEntryFound ) { - entryToWrite = i.next(); - if ( !deletedItems.contains( entryToWrite.first ) ) - nextEntryFound = true; - } - - if ( nextEntryFound ) { - if ( i.hasNext() ) { // Read the next entry to determine the size. - MsgInfo entryAfterEntryToWrite = i.next(); - quint64 entryToWriteSize = entryAfterEntryToWrite.first - entryToWrite.first - 1; - quint64 mapSize = entryAfterEntryToWrite.first - writeOffset - 1; - - // Now map writeOffSet to entryAfterEntryToWrite offset into mem. - uchar *memArea = d->mMboxFile.map( writeOffset, mapSize ); - - // Now read the entry that must be moved to writeOffset. - quint64 startOffset = entryToWrite.first - writeOffset; - char *start = reinterpret_cast( memArea + startOffset ); - QByteArray entryToWriteData( start, entryToWriteSize ); - - memcpy( memArea, entryToWriteData.constData(), entryToWriteSize ); - - d->mMboxFile.unmap( memArea ); - - resultingEntryList << MsgInfo( writeOffset, entryToWrite.second ); - writeOffset += entryToWriteSize + 1; - } else { // entryToWrite is the last entry in the file - quint64 entryToWriteSize = d->mMboxFile.size() - entryToWrite.first - 1; - quint64 mapSize = d->mMboxFile.size() - writeOffset - 1; - - // Now map writeOffSet upto mapSize into mem. - uchar *memArea = d->mMboxFile.map( writeOffset, mapSize ); - - quint64 startOffset = entryToWrite.first - writeOffset; - char *start = reinterpret_cast( memArea + startOffset ); - QByteArray entryToWriteData( start, entryToWriteSize ); - - memcpy( memArea, entryToWriteData.constData(), entryToWriteSize ); - - d->mMboxFile.unmap( memArea ); - - resultingEntryList << MsgInfo( writeOffset, entryToWrite.second ); - writeOffset += entryToWriteSize + 1; - - // Chop off the remaining bytes. - d->mMboxFile.resize( writeOffset ); - } - } else { - // All entries after writeOffset are marked as deleted so resize the - // file. - d->mMboxFile.resize( writeOffset ); - } + entrySize = i.next().first - entry.first - 1; + i.previous(); // Go back to make sure that we also handle the next entry. } else { - // It is the last entry of the file so just chop off the remaining content - // from writeOffset to end of file. - d->mMboxFile.resize( writeOffset ); + entrySize = origFileSize - entry.first - 1; } - } else { + + Q_ASSERT( entrySize > 0 ); // MBox entries really cannot have a size <= 0; + + // we map the whole area of the file starting at the writeOffset up to the + // message that have to be moved into memory. This includes eventually the + // messages that are the deleted between the first deleted message + // encountered and the message that has to be moved. + quint64 mapSize = entry.first + entrySize - writeOffset; + + // Now map writeOffSet + mapSize into mem. + uchar *memArea = d->mMboxFile.map( writeOffset, mapSize ); + + // Now read the entry that must be moved to writeOffset. + quint64 startOffset = entry.first - writeOffset; + char *start = reinterpret_cast( memArea + startOffset ); + QByteArray entryToWriteData( start, entrySize ); + + memcpy( memArea, entryToWriteData.constData(), entrySize ); + + d->mMboxFile.unmap( memArea ); + resultingEntryList << MsgInfo( writeOffset, entry.second ); + writeOffset += entrySize + 1; + } else if ( !deletedItems.contains( entry.first ) ) { + // Unmoved and not deleted entry, can only occure before the first deleted + // entry. + Q_ASSERT( !writeOffSetInitialized ); + resultingEntryList << entry; } } + // Chop off remaining entry bits. + d->mMboxFile.resize( writeOffset ); d->mEntries = resultingEntryList; kDebug() << "Purge comleted successfully, unlocking the file."; return unlock(); // FIXME: What if this fails? It will return false but the // file has changed. } KMime::Message *MBox::readEntry(quint64 offset) { bool wasLocked = d->mFileLocked; if ( ! wasLocked ) if ( ! lock() ) return 0; // TODO: Add error handling in case locking failed. Q_ASSERT( d->mFileLocked ); Q_ASSERT( d->mMboxFile.isOpen() ); Q_ASSERT( d->mMboxFile.size() > 0 ); if ( offset > static_cast( d->mMboxFile.size() ) ) { unlock(); return 0; } d->mMboxFile.seek(offset); QByteArray line = d->mMboxFile.readLine(); QRegExp regexp( sMBoxSeperatorRegExp ); if (regexp.indexIn(line) < 0) { unlock(); return 0; // The file is messed up or the index is incorrect. } QByteArray message; line = d->mMboxFile.readLine(); while (regexp.indexIn(line) < 0 && !d->mMboxFile.atEnd()) { message += line; line = d->mMboxFile.readLine(); } // Remove te last '\n' added by writeEntry. if (message.endsWith('\n')) message.chop(1); unescapeFrom(message.data(), message.size()); if ( ! wasLocked ) { const bool unlocked = unlock(); Q_ASSERT( unlocked ); Q_UNUSED( unlocked ); } KMime::Message *mail = new KMime::Message(); mail->setContent( KMime::CRLFtoLF( message ) ); mail->parse(); return mail; } QByteArray MBox::readEntryHeaders(quint64 offset) { bool wasLocked = d->mFileLocked; if ( ! wasLocked ) lock(); Q_ASSERT( d->mFileLocked ); Q_ASSERT(d->mMboxFile.isOpen()); Q_ASSERT(d->mMboxFile.size() > 0); Q_ASSERT(static_cast(d->mMboxFile.size()) > offset); d->mMboxFile.seek(offset); QByteArray headers; QByteArray line = d->mMboxFile.readLine(); while (!line[0] == '\n') { headers += line; line = d->mMboxFile.readLine(); } if ( ! wasLocked ) unlock(); return headers; } bool MBox::save( const QString &fileName ) { if ( !fileName.isEmpty() && KUrl( fileName ).path() != d->mMboxFile.fileName() ) { // File saved != file loaded from return false; // FIXME: Implement this case } if ( d->mAppendedEntries.size() == 0 ) return true; // Nothing to do. if ( !lock() ) return false; Q_ASSERT( d->mMboxFile.isOpen() ); d->mMboxFile.seek( d->mMboxFile.size() ); d->mMboxFile.write( d->mAppendedEntries ); d->mAppendedEntries.clear(); return unlock(); } bool MBox::setLockType(LockType ltype) { if (d->mFileLocked) { kDebug() << "File is currently locked."; return false; // Don't change the method if the file is currently locked. } switch ( ltype ) { case KDELockFile: kDebug() << "KLockFile not supported yet"; // FIXME return false; case ProcmailLockfile: if ( KStandardDirs::findExe( "lockfile" ).isEmpty() ) { kDebug() << "Could not find the lockfile executable"; return false; } break; case MuttDotlock: // fall through case MuttDotlockPrivileged: if (KStandardDirs::findExe("mutt_dotlock").isEmpty()) { kDebug() << "Could not find the mutt_dotlock executable"; return false; } break; default: break; // We assume fcntl available and lock_none doesn't need a check. } d->mLockType = ltype; return true; } void MBox::setLockFile(const QString &lockFile) { d->mLockFileName = lockFile; } bool MBox::unlock() { int rc = 0; QStringList args; switch( d->mLockType ) { case KDELockFile: // FIXME //d->mLock.unlock(); break; case ProcmailLockfile: // QFile::remove returns true on succes so negate the result. if (!d->mLockFileName.isEmpty()) rc = !QFile(d->mLockFileName).remove(); else rc = !QFile(d->mMboxFile.fileName() + ".lock").remove(); break; case MuttDotlock: args << "-u" << quoteAndEncode(d->mMboxFile.fileName()); rc = QProcess::execute("mutt_dotlock", args); break; case MuttDotlockPrivileged: args << "-u" << "-p" << quoteAndEncode(d->mMboxFile.fileName()); rc = QProcess::execute("mutt_dotlock", args); break; case None: // Fall through. default: break; } if ( rc == 0 ) // Unlocking succeeded d->mFileLocked = false; d->mMboxFile.close(); return !d->mFileLocked; } /// private methods bool MBox::open() { if ( d->mMboxFile.isOpen() ) return true; // already open if ( !d->mMboxFile.open( QIODevice::ReadWrite ) ) { // messages file kDebug() << "Cannot open mbox file `" << d->mMboxFile.fileName() << "' FileError:" << d->mMboxFile.error(); return false; } return true; } QByteArray MBox::mboxMessageSeparator(const QByteArray &msg) { KMime::Message mail; mail.setHead(KMime::CRLFtoLF(msg)); mail.parse(); QByteArray seperator = "From "; KMime::Headers::From *from = mail.from(false); if (!from || from->addresses().isEmpty()) seperator += "unknown@unknown.invalid"; else seperator += from->addresses().first() + " "; KMime::Headers::Date *date = mail.date(false); if (!date || date->isEmpty()) seperator += QDateTime::currentDateTime().toString(Qt::TextDate).toUtf8() + '\n'; else seperator += date->as7BitString(false) + '\n'; return seperator; } #define STRDIM(x) (sizeof(x)/sizeof(*x)-1) QByteArray MBox::escapeFrom(const QByteArray &str) { const unsigned int strLen = str.length(); if ( strLen <= STRDIM("From ") ) return str; // worst case: \nFrom_\nFrom_\nFrom_... => grows to 7/6 QByteArray result(int( strLen + 5 ) / 6 * 7 + 1, '\0'); const char * s = str.data(); const char * const e = s + strLen - STRDIM("From "); char * d = result.data(); bool onlyAnglesAfterLF = false; // dont' match ^From_ while ( s < e ) { switch ( *s ) { case '\n': onlyAnglesAfterLF = true; break; case '>': break; case 'F': if ( onlyAnglesAfterLF && qstrncmp( s+1, "rom ", STRDIM("rom ") ) == 0 ) *d++ = '>'; // fall through default: onlyAnglesAfterLF = false; break; } *d++ = *s++; } while ( s < str.data() + strLen ) *d++ = *s++; result.truncate( d - result.data() ); return result; } // performs (\n|^)>{n}From_ -> \1>{n-1}From_ conversion void MBox::unescapeFrom(char* str, size_t strLen) { if (!str) return; if ( strLen <= STRDIM(">From ") ) return; // yes, *d++ = *s++ is a no-op as long as d == s (until after the // first >From_), but writes are cheap compared to reads and the // data is already in the cache from the read, so special-casing // might even be slower... const char * s = str; char * d = str; const char * const e = str + strLen - STRDIM(">From "); while ( s < e ) { if ( *s == '\n' && *(s+1) == '>' ) { // we can do the lookahead, since e is 6 chars from the end! *d++ = *s++; // == '\n' *d++ = *s++; // == '>' while ( s < e && *s == '>' ) *d++ = *s++; if ( qstrncmp( s, "From ", STRDIM("From ") ) == 0 ) --d; } *d++ = *s++; // yes, s might be e here, but e is not the end :-) } // copy the rest: while ( s < str + strLen ) *d++ = *s++; if ( d < s ) // only NUL-terminate if it's shorter *d = 0; } #undef STRDIM diff --git a/kmbox/tests/mboxtest.cpp b/kmbox/tests/mboxtest.cpp index 6ebeb4ebf..6dafa779e 100644 --- a/kmbox/tests/mboxtest.cpp +++ b/kmbox/tests/mboxtest.cpp @@ -1,295 +1,354 @@ /* Copyright (C) 2009 Bertjan Broeksema This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "mboxtest.h" #include "mboxtest.moc" #include #include #include #include #include QTEST_KDEMAIN_CORE(MboxTest) #include "test-entries.h" static const char * testDir = "libmbox-unit-test"; static const char * testFile = "test-mbox-file"; static const char * testLockFile = "test-mbox-lock-file"; QString MboxTest::fileName() { return mTempDir->name() + testFile; } QString MboxTest::lockFileName() { return mTempDir->name() + testLockFile; } void MboxTest::removeTestFile() { QFile file( fileName() ); file.remove(); QVERIFY( !file.exists() ); } void MboxTest::initTestCase() { mTempDir = new KTempDir( KStandardDirs::locateLocal("tmp", testDir ) ); QDir temp(mTempDir->name()); QVERIFY(temp.exists()); QFile mboxfile( fileName() ); mboxfile.open( QIODevice::WriteOnly ); mboxfile.close(); QVERIFY(mboxfile.exists()); mMail1 = MessagePtr( new KMime::Message ); mMail1->setContent( KMime::CRLFtoLF( sEntry1 ) ); mMail1->parse(); mMail2 = MessagePtr( new KMime::Message ); mMail2->setContent( KMime::CRLFtoLF( sEntry2 ) ); mMail2->parse(); } void MboxTest::testSetLockMethod() { MBox mbox1; if ( !KStandardDirs::findExe( "lockfile" ).isEmpty() ) { QVERIFY( mbox1.setLockType(MBox::ProcmailLockfile) ); } else { QVERIFY( !mbox1.setLockType( MBox::ProcmailLockfile ) ); } if ( !KStandardDirs::findExe("mutt_dotlock").isEmpty() ) { QVERIFY( mbox1.setLockType( MBox::MuttDotlock ) ); QVERIFY( mbox1.setLockType( MBox::MuttDotlockPrivileged ) ); } else { QVERIFY( !mbox1.setLockType( MBox::MuttDotlock ) ); QVERIFY( !mbox1.setLockType( MBox::MuttDotlockPrivileged ) ); } QVERIFY( mbox1.setLockType( MBox::None ) ); QEXPECT_FAIL("", "KDELockFile method is not yet implmented", Continue); QVERIFY( mbox1.setLockType( MBox::KDELockFile ) ); } void MboxTest::testLockBeforeLoad() { // Should fail because it's not known which file to lock. MBox mbox; if ( !KStandardDirs::findExe( "lockfile" ).isEmpty() ) { QVERIFY( mbox.setLockType(MBox::ProcmailLockfile) ); QVERIFY( !mbox.lock() ); } if ( !KStandardDirs::findExe("mutt_dotlock").isEmpty() ) { QVERIFY( mbox.setLockType( MBox::MuttDotlock ) ); QVERIFY( !mbox.lock() ); QVERIFY( mbox.setLockType( MBox::MuttDotlockPrivileged ) ); QVERIFY( !mbox.lock() ); } QVERIFY( mbox.setLockType( MBox::None ) ); QVERIFY( !mbox.lock() ); QEXPECT_FAIL("", "KDELockFile method is not yet implmented", Continue); QVERIFY( mbox.setLockType( MBox::KDELockFile ) ); QVERIFY( !mbox.lock() ); } void MboxTest::testProcMailLock() { // It really only makes sense to test this if the lockfile executable can be // found. MBox mbox; if ( !mbox.setLockType( MBox::ProcmailLockfile ) ) { QEXPECT_FAIL( "", "This test only works when procmail is installed.", Abort ); QVERIFY( false ); } QVERIFY( mbox.load( fileName() ) ); // By default the filename is used as part of the lockfile filename. QVERIFY( !QFile( fileName() + ".lock" ).exists() ); QVERIFY( mbox.lock() ); QVERIFY( QFile( fileName() + ".lock" ).exists() ); QVERIFY( mbox.unlock() ); QVERIFY( !QFile( fileName() + ".lock" ).exists() ); mbox.setLockFile( lockFileName() ); QVERIFY( !QFile( lockFileName() ).exists() ); QVERIFY( mbox.lock() ); QVERIFY( QFile( lockFileName() ).exists() ); QVERIFY( mbox.unlock() ); QVERIFY( !QFile( lockFileName() ).exists() ); } void MboxTest::testAppend() { QFileInfo info( fileName() ); QCOMPARE( info.size(), static_cast( 0 ) ); MBox mbox; mbox.setLockType( MBox::None ); // When no file is loaded no entries should get added to the mbox. QCOMPARE( mbox.entryList().size(), 0 ); QCOMPARE( mbox.appendEntry( mMail1 ), static_cast( -1 ) ); QCOMPARE( mbox.entryList().size(), 0 ); QVERIFY( mbox.load( fileName() ) ); // First message added to an emtpy file should be at offset 0 QCOMPARE( mbox.entryList().size(), 0 ); QCOMPARE( mbox.appendEntry( mMail1 ), static_cast( 0 ) ); QCOMPARE( mbox.entryList().size(), 1 ); QCOMPARE( mbox.entryList().first().second, static_cast( sEntry1.size() ) ); QVERIFY( mbox.appendEntry( mMail2 ) > sEntry1.size() ); QCOMPARE( mbox.entryList().size(), 2 ); QCOMPARE( mbox.entryList().last().second, static_cast( sEntry2.size() ) ); } void MboxTest::testSaveAndLoad() { removeTestFile(); MBox mbox; QVERIFY( mbox.setLockType( MBox::None ) ); QVERIFY( mbox.load( fileName() ) ); mbox.appendEntry( mMail1 ); mbox.appendEntry( mMail2 ); QList infos1 = mbox.entryList(); QCOMPARE( infos1.size(), 2 ); QVERIFY( mbox.save() ); QVERIFY( QFileInfo( fileName() ).exists() ); QList infos2 = mbox.entryList(); QCOMPARE( infos2.size(), 2 ); for ( int i = 0; i < 2; ++i ) { QCOMPARE( infos1.at(i).first, infos2.at(i).first ); QCOMPARE( infos1.at(i).second, infos2.at(i).second ); } MBox mbox2; QVERIFY( mbox2.setLockType( MBox::None ) ); QVERIFY( mbox2.load( fileName() ) ); QList infos3 = mbox2.entryList(); QCOMPARE( infos3.size(), 2 ); for ( int i = 0; i < 2; ++i ) { QCOMPARE( infos3.at(i).first, infos2.at(i).first ); quint64 minSize = infos2.at(i).second; quint64 maxSize = infos2.at(i).second + 1; QVERIFY( infos3.at(i).second >= minSize ); QVERIFY( infos3.at(i).second <= maxSize ); } } void MboxTest::testBlankLines() { for ( int i = 0; i < 5; ++i ) { removeTestFile(); MessagePtr mail = MessagePtr( new KMime::Message ); mail->setContent( KMime::CRLFtoLF( sEntry1 + QByteArray( i, '\n' ) ) ); mail->parse(); MBox mbox1; QVERIFY( mbox1.setLockType( MBox::None ) ); QVERIFY( mbox1.load( fileName() ) ); mbox1.appendEntry( mail ); mbox1.appendEntry( mail ); mbox1.appendEntry( mail ); mbox1.save(); MBox mbox2; QVERIFY( mbox1.setLockType( MBox::None ) ); QVERIFY( mbox1.load( fileName() ) ); QCOMPARE( mbox1.entryList().size(), 3 ); quint64 minSize = sEntry1.size() + i - 1; // Possibly on '\n' falls off. quint64 maxSize = sEntry1.size() + i; for ( int i = 0; i < 3; ++i ) { QVERIFY( mbox1.entryList().at( i ).second >= minSize ); QVERIFY( mbox1.entryList().at( i ).second <= maxSize ); } } } void MboxTest::testEntries() { removeTestFile(); MBox mbox1; QVERIFY( mbox1.setLockType( MBox::None ) ); QVERIFY( mbox1.load( fileName() ) ); mbox1.appendEntry( mMail1 ); mbox1.appendEntry( mMail2 ); mbox1.appendEntry( mMail1 ); QList infos = mbox1.entryList(); QCOMPARE( infos.size() , 3 ); QSet deletedEntries; deletedEntries << infos.at( 0 ).first; QList infos2 = mbox1.entryList( deletedEntries ); QCOMPARE( infos2.size() , 2 ); QVERIFY( infos2.first().first != infos.first().first ); QVERIFY( infos2.last().first != infos.first().first ); deletedEntries << infos.at( 1 ).first; infos2 = mbox1.entryList( deletedEntries ); QCOMPARE( infos2.size() , 1 ); QVERIFY( infos2.first().first != infos.at( 0 ).first ); QVERIFY( infos2.first().first != infos.at( 1 ).first ); deletedEntries << infos.at( 2 ).first; infos2 = mbox1.entryList( deletedEntries ); QCOMPARE( infos2.size() , 0 ); QVERIFY( !deletedEntries.contains( 10 ) ); // some random offset infos2 = mbox1.entryList( QSet() << 10 ); QCOMPARE( infos2.size() , 3 ); QCOMPARE( infos2.at( 0 ).first, infos.at( 0 ).first ); QCOMPARE( infos2.at( 1 ).first, infos.at( 1 ).first ); QCOMPARE( infos2.at( 2 ).first, infos.at( 2 ).first ); } +void MboxTest::testPurge() +{ + MBox mbox1; + QVERIFY( mbox1.setLockType( MBox::None ) ); + QVERIFY( mbox1.load( fileName() ) ); + mbox1.appendEntry( mMail1 ); + mbox1.appendEntry( mMail1 ); + mbox1.appendEntry( mMail1 ); + QVERIFY( mbox1.save() ); + + QList list = mbox1.entryList(); + + // First test: Delete only the first (all messages afterwards have to be moved). + mbox1.purge( QSet() << list.first().first ); + + MBox mbox2; + QVERIFY( mbox2.load( fileName() ) ); + QList list2 = mbox2.entryList(); + QCOMPARE( list2.size(), 2 ); // Is a message actually gone? + + quint64 newOffsetSecondMessage = list.last().first - list.at( 1 ).first; + + QCOMPARE( list2.first().first, static_cast( 0 ) ); + QCOMPARE( list2.last().first, newOffsetSecondMessage ); + + // Second test: Delete the first two (the last message have to be moved). + removeTestFile(); + + QVERIFY( mbox1.load( fileName() ) ); + mbox1.appendEntry( mMail1 ); + mbox1.appendEntry( mMail1 ); + mbox1.appendEntry( mMail1 ); + QVERIFY( mbox1.save() ); + + list = mbox1.entryList(); + + mbox1.purge( QSet() << list.at( 0 ).first << list.at( 1 ).first ); + QVERIFY( mbox2.load( fileName() ) ); + list2 = mbox2.entryList(); + QCOMPARE( list2.size(), 1 ); // Are the messages actually gone? + QCOMPARE( list2.first().first, static_cast( 0 ) ); + + // Third test: Delete all messages. + removeTestFile(); + + QVERIFY( mbox1.load( fileName() ) ); + mbox1.appendEntry( mMail1 ); + mbox1.appendEntry( mMail1 ); + mbox1.appendEntry( mMail1 ); + QVERIFY( mbox1.save() ); + + list = mbox1.entryList(); + + mbox1.purge( QSet() << list.at( 0 ).first << list.at( 1 ).first << list.at( 2 ).first ); + QVERIFY( mbox2.load( fileName() ) ); + list2 = mbox2.entryList(); + QCOMPARE( list2.size(), 0 ); // Are the messages actually gone? +} + void MboxTest::cleanupTestCase() { mTempDir->unlink(); } diff --git a/kmbox/tests/mboxtest.h b/kmbox/tests/mboxtest.h index 4f52fb1be..737df5700 100644 --- a/kmbox/tests/mboxtest.h +++ b/kmbox/tests/mboxtest.h @@ -1,54 +1,55 @@ /* Copyright (c) 2009 Bertjan Broeksema This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef MBOXTEST_H #define MBOXTEST_H #include #include "../mbox.h" class KTempDir; class MboxTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void testSetLockMethod(); void testLockBeforeLoad(); void testProcMailLock(); void testAppend(); void testSaveAndLoad(); void testBlankLines(); void cleanupTestCase(); void testEntries(); + void testPurge(); private: QString fileName(); QString lockFileName(); void removeTestFile(); private: KTempDir *mTempDir; MessagePtr mMail1; MessagePtr mMail2; }; #endif // MBOXTEST_H