kde-playground/kdepimlibs/kmbox/mbox.cpp
2015-04-14 21:49:29 +00:00

694 lines
19 KiB
C++

/*
Copyright (c) 1996-1998 Stefan Taferner <taferner@kde.org>
Copyright (c) 2009 Bertjan Broeksema <broeksema@kde.org>
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 line for Stefan Taferner.
Bertjan Broeksema, april 2009
*/
#include "mbox.h"
#include "mbox_p.h"
#include "mboxentry_p.h"
#include <KDebug>
#include <KStandardDirs>
#include <KUrl>
#include <QtCore/QBuffer>
#include <QtCore/QProcess>
using namespace KMBox;
/// public methods.
MBox::MBox()
: d( new MBoxPrivate( this ) )
{
// Set some sane defaults
d->mFileLocked = false;
d->mLockType = None;
d->mUnlockTimer.setInterval( 0 );
d->mUnlockTimer.setSingleShot( true );
}
MBox::~MBox()
{
if ( d->mFileLocked ) {
unlock();
}
d->close();
delete d;
}
// Appended entries works as follows: When an mbox file is loaded from disk,
// d->mInitialMboxFileSize is set to the file size at that moment. New entries
// are stored in memory (d->mAppendedEntries). The initial file size and the size
// of the buffer determine the offset for the next message to append.
MBoxEntry MBox::appendMessage( const KMime::Message::Ptr &entry )
{
// It doesn't make sense to add entries when we don't have an reference file.
Q_ASSERT( !d->mMboxFile.fileName().isEmpty() );
const QByteArray rawEntry = MBoxPrivate::escapeFrom( entry->encodedContent() );
if ( rawEntry.size() <= 0 ) {
kDebug() << "Message added to folder `" << d->mMboxFile.fileName()
<< "' contains no data. Ignoring it.";
return MBoxEntry();
}
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;
}
}
}
const QByteArray separator = MBoxPrivate::mboxMessageSeparator( rawEntry );
d->mAppendedEntries.append( separator );
d->mAppendedEntries.append( rawEntry );
if ( rawEntry[rawEntry.size() - 1] != '\n' ) {
d->mAppendedEntries.append( "\n\n" );
} else {
d->mAppendedEntries.append( "\n" );
}
MBoxEntry resultEntry;
resultEntry.d->mOffset = d->mInitialMboxFileSize + nextOffset;
resultEntry.d->mMessageSize = rawEntry.size();
resultEntry.d->mSeparatorSize = separator.size();
d->mEntries << resultEntry;
return resultEntry;
}
MBoxEntry::List MBox::entries( const MBoxEntry::List &deletedEntries ) const
{
if ( deletedEntries.isEmpty() ) {
// fast path
return d->mEntries;
}
MBoxEntry::List result;
foreach ( const MBoxEntry &entry, d->mEntries ) {
if ( !deletedEntries.contains( entry ) ) {
result << entry;
}
}
return result;
}
QString MBox::fileName() const
{
return d->mMboxFile.fileName();
}
bool MBox::load( const QString &fileName )
{
if ( d->mFileLocked ) {
return false;
}
d->initLoad( fileName );
if ( !lock() ) {
kDebug() << "Failed to lock";
return false;
}
d->mInitialMboxFileSize = d->mMboxFile.size(); // AFTER the file has been locked
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 atEnd, use mail only if there was a separator line at all,
// otherwise it's not a valid mbox
if ( d->isMBoxSeparator( line ) ||
( d->mMboxFile.atEnd() && ( prevSeparator.size() != 0 ) ) ) {
// if we are the at the file end, update pos to not forget the last line
if ( d->mMboxFile.atEnd() ) {
pos = d->mMboxFile.pos();
}
// 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.
MBoxEntry entry;
entry.d->mOffset = offs;
entry.d->mSeparatorSize = prevSeparator.size();
entry.d->mMessageSize = msgSize - 1;
// Don't add the separator size and the newline up to the message size.
entry.d->mMessageSize -= prevSeparator.size() + 1;
d->mEntries << entry;
}
if ( d->isMBoxSeparator( line ) ) {
prevSeparator = line;
}
offs += msgSize; // Mark the beginning of the next message.
}
}
// FIXME: What if unlock fails?
// if no separator was found, the file is still valid if it is empty
return unlock() && ( ( prevSeparator.size() != 0 ) || ( d->mMboxFile.size() == 0 ) );
}
bool MBox::lock()
{
if ( d->mMboxFile.fileName().isEmpty() ) {
return false; // We cannot lock if there is no file loaded.
}
// We can't load another file when the mbox currently is locked so if d->mFileLocked
// is true atm just return true.
if ( locked() ) {
return true;
}
if ( d->mLockType == None ) {
d->mFileLocked = true;
if ( d->open() ) {
d->startTimerIfNeeded();
return true;
}
d->mFileLocked = false;
return false;
}
QStringList args;
int rc = 0;
switch ( d->mLockType ) {
case ProcmailLockfile:
args << QLatin1String( "-l20" ) << QLatin1String( "-r5" );
if ( !d->mLockFileName.isEmpty() ) {
args << QString::fromLocal8Bit( QFile::encodeName( d->mLockFileName ) );
} else {
args << QString::fromLocal8Bit( QFile::encodeName( d->mMboxFile.fileName() +
QLatin1String( ".lock" ) ) );
}
rc = QProcess::execute( QLatin1String( "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 << QString::fromLocal8Bit( QFile::encodeName( d->mMboxFile.fileName() ) );
rc = QProcess::execute( QLatin1String( "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 << QLatin1String( "-p" )
<< QString::fromLocal8Bit( QFile::encodeName( d->mMboxFile.fileName() ) );
rc = QProcess::execute( QLatin1String( "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 ( !d->open() ) {
const bool unlocked = unlock();
Q_ASSERT( unlocked ); // If this fails we're in trouble.
Q_UNUSED( unlocked );
}
}
d->startTimerIfNeeded();
return d->mFileLocked;
}
bool MBox::locked() const
{
return d->mFileLocked;
}
static bool lessThanByOffset( const MBoxEntry &left, const MBoxEntry &right )
{
return left.messageOffset() < right.messageOffset();
}
bool MBox::purge( const MBoxEntry::List &deletedEntries, QList<MBoxEntry::Pair> *movedEntries )
{
if ( d->mMboxFile.fileName().isEmpty() ) {
return false; // No file loaded yet.
}
if ( deletedEntries.isEmpty() ) {
return true; // Nothing to do.
}
if ( !lock() ) {
return false;
}
foreach ( const MBoxEntry &entry, deletedEntries ) {
d->mMboxFile.seek( entry.messageOffset() );
const QByteArray line = d->mMboxFile.readLine();
if ( !d->isMBoxSeparator( line ) ) {
qDebug() << "Found invalid separator at:" << entry.messageOffset();
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 ( deletedEntries.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;
MBoxEntry::List resultingEntryList;
QList<MBoxEntry::Pair> tmpMovedEntries;
quint64 origFileSize = d->mMboxFile.size();
QListIterator<MBoxEntry> i( d->mEntries );
while ( i.hasNext() ) {
MBoxEntry entry = i.next();
if ( deletedEntries.contains( entry ) && !writeOffSetInitialized ) {
writeOffset = entry.messageOffset();
writeOffSetInitialized = true;
} else if ( writeOffSetInitialized &&
writeOffset < entry.messageOffset() &&
!deletedEntries.contains( entry ) ) {
// 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() ) {
entrySize = i.next().messageOffset() - entry.messageOffset();
i.previous(); // Go back to make sure that we also handle the next entry.
} else {
entrySize = origFileSize - entry.messageOffset();
}
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.messageOffset() + 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.messageOffset() - writeOffset;
memmove( memArea, memArea + startOffset, entrySize );
d->mMboxFile.unmap( memArea );
MBoxEntry resultEntry;
resultEntry.d->mOffset = writeOffset;
resultEntry.d->mSeparatorSize = entry.separatorSize();
resultEntry.d->mMessageSize = entry.messageSize();
resultingEntryList << resultEntry;
tmpMovedEntries << MBoxEntry::Pair( MBoxEntry( entry.messageOffset() ),
MBoxEntry( resultEntry.messageOffset() ) );
writeOffset += entrySize;
} else if ( !deletedEntries.contains( entry ) ) {
// Unmoved and not deleted entry, can only ocure 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.";
if ( movedEntries ) {
*movedEntries = tmpMovedEntries;
}
return unlock(); // FIXME: What if this fails? It will return false but the
// file has changed.
}
QByteArray MBox::readRawMessage( const MBoxEntry &entry )
{
const bool wasLocked = locked();
if ( !wasLocked ) {
if ( !lock() ) {
return QByteArray();
}
}
// TODO: Add error handling in case locking failed.
quint64 offset = entry.messageOffset();
Q_ASSERT( d->mFileLocked );
Q_ASSERT( d->mMboxFile.isOpen() );
Q_ASSERT( ( d->mInitialMboxFileSize + d->mAppendedEntries.size() ) > offset );
QByteArray message;
if ( offset < d->mInitialMboxFileSize ) {
d->mMboxFile.seek( offset );
QByteArray line = d->mMboxFile.readLine();
if ( !d->isMBoxSeparator( line ) ) {
kDebug() << "[MBox::readEntry] Invalid entry at:" << offset;
if ( !wasLocked ) {
unlock();
}
return QByteArray(); // The file is messed up or the index is incorrect.
}
line = d->mMboxFile.readLine();
while ( !d->isMBoxSeparator( line ) ) {
message += line;
if ( d->mMboxFile.atEnd() ) {
break;
}
line = d->mMboxFile.readLine();
}
} else {
offset -= d->mInitialMboxFileSize;
if ( offset > static_cast<quint64>( d->mAppendedEntries.size() ) ) {
if ( !wasLocked ) {
unlock();
}
return QByteArray();
}
QBuffer buffer( &( d->mAppendedEntries ) );
buffer.open( QIODevice::ReadOnly );
buffer.seek( offset );
QByteArray line = buffer.readLine();
if ( !d->isMBoxSeparator( line ) ) {
kDebug() << "[MBox::readEntry] Invalid appended entry at:" << offset;
if ( !wasLocked ) {
unlock();
}
return QByteArray(); // The file is messed up or the index is incorrect.
}
line = buffer.readLine();
while ( !d->isMBoxSeparator( line ) && !buffer.atEnd() ) {
message += line;
line = buffer.readLine();
}
}
// Remove te last '\n' added by writeEntry.
if ( message.endsWith( '\n' ) ) {
message.chop( 1 );
}
MBoxPrivate::unescapeFrom( message.data(), message.size() );
if ( !wasLocked ) {
if ( !d->startTimerIfNeeded() ) {
const bool unlocked = unlock();
Q_ASSERT( unlocked );
Q_UNUSED( unlocked );
}
}
return message;
}
KMime::Message *MBox::readMessage( const MBoxEntry &entry )
{
const QByteArray message = readRawMessage( entry );
if ( message.isEmpty() ) {
return 0;
}
KMime::Message *mail = new KMime::Message();
mail->setContent( KMime::CRLFtoLF( message ) );
mail->parse();
return mail;
}
QByteArray MBox::readMessageHeaders( const MBoxEntry &entry )
{
const bool wasLocked = d->mFileLocked;
if ( !wasLocked ) {
if (!lock()) {
kDebug() << "Failed to lock";
return QByteArray();
}
}
const quint64 offset = entry.messageOffset();
Q_ASSERT( d->mFileLocked );
Q_ASSERT( d->mMboxFile.isOpen() );
Q_ASSERT( ( d->mInitialMboxFileSize + d->mAppendedEntries.size() ) > offset );
QByteArray headers;
if ( offset < d->mInitialMboxFileSize ) {
d->mMboxFile.seek( offset );
QByteArray line = d->mMboxFile.readLine();
while ( line[0] != '\n' && !d->mMboxFile.atEnd() ) {
headers += line;
line = d->mMboxFile.readLine();
}
} else {
QBuffer buffer( &( d->mAppendedEntries ) );
buffer.open( QIODevice::ReadOnly );
buffer.seek( offset - d->mInitialMboxFileSize );
QByteArray line = buffer.readLine();
while ( line[0] != '\n' && !buffer.atEnd() ) {
headers += line;
line = buffer.readLine();
}
}
if ( !wasLocked ) {
unlock();
}
return headers;
}
bool MBox::save( const QString &fileName )
{
if ( !fileName.isEmpty() && KUrl( fileName ).toLocalFile() != d->mMboxFile.fileName() ) {
if ( !d->mMboxFile.copy( fileName ) ) {
return false;
}
if ( d->mAppendedEntries.size() == 0 ) {
return true; // Nothing to do
}
QFile otherFile( fileName );
Q_ASSERT( otherFile.exists() );
if ( !otherFile.open( QIODevice::ReadWrite ) ) {
return false;
}
otherFile.seek( d->mMboxFile.size() );
otherFile.write( d->mAppendedEntries );
// Don't clear mAppendedEntries and don't update mInitialFileSize. These
// are still valid for the original file.
return true;
}
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();
d->mInitialMboxFileSize = d->mMboxFile.size();
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 ProcmailLockfile:
if ( KStandardDirs::findExe( QLatin1String( "lockfile" ) ).isEmpty() ) {
kDebug() << "Could not find the lockfile executable";
return false;
}
break;
case MuttDotlock: // fall through
case MuttDotlockPrivileged:
if ( KStandardDirs::findExe( QLatin1String( "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;
}
void MBox::setUnlockTimeout( int msec )
{
d->mUnlockTimer.setInterval( msec );
}
bool MBox::unlock()
{
if ( d->mLockType == None && !d->mFileLocked ) {
d->mFileLocked = false;
d->mMboxFile.close();
return true;
}
int rc = 0;
QStringList args;
switch ( d->mLockType ) {
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() + QLatin1String( ".lock" ) ).remove();
}
break;
case MuttDotlock:
args << QLatin1String( "-u" )
<< QString::fromLocal8Bit( QFile::encodeName( d->mMboxFile.fileName() ) );
rc = QProcess::execute( QLatin1String( "mutt_dotlock" ), args );
break;
case MuttDotlockPrivileged:
args << QLatin1String( "-u" ) << QLatin1String( "-p" )
<< QString::fromLocal8Bit( QFile::encodeName( d->mMboxFile.fileName() ) );
rc = QProcess::execute( QLatin1String( "mutt_dotlock" ), args );
break;
case None: // Fall through.
default:
break;
}
if ( rc == 0 ) { // Unlocking succeeded
d->mFileLocked = false;
}
d->mMboxFile.close();
return !d->mFileLocked;
}