diff options
Diffstat (limited to 'kmail/backupjob.cpp')
-rw-r--r-- | kmail/backupjob.cpp | 500 |
1 files changed, 500 insertions, 0 deletions
diff --git a/kmail/backupjob.cpp b/kmail/backupjob.cpp new file mode 100644 index 000000000..fd53997b3 --- /dev/null +++ b/kmail/backupjob.cpp @@ -0,0 +1,500 @@ +/* Copyright 2009 Klarälvdalens Datakonsult AB + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License or (at your option) version 3 or any later version + accepted by the membership of KDE e.V. (or its successor approved + by the membership of KDE e.V.), which shall act as a proxy + defined in Section 14 of version 3 of the license. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ +#include "backupjob.h" + +#include "kmmsgdict.h" +#include "kmfolder.h" +#include "kmfoldercachedimap.h" +#include "kmfolderdir.h" +#include "folderutil.h" + +#include "progressmanager.h" + +#include "kzip.h" +#include "ktar.h" +#include "kmessagebox.h" + +#include "tqfile.h" +#include "tqfileinfo.h" +#include "tqstringlist.h" + +using namespace KMail; + +BackupJob::BackupJob( TQWidget *parent ) + : TQObject( parent ), + mArchiveType( Zip ), + mRootFolder( 0 ), + mArchive( 0 ), + mParentWidget( parent ), + mCurrentFolderOpen( false ), + mArchivedMessages( 0 ), + mArchivedSize( 0 ), + mProgressItem( 0 ), + mAborted( false ), + mDeleteFoldersAfterCompletion( false ), + mCurrentFolder( 0 ), + mCurrentMessage( 0 ), + mCurrentJob( 0 ) +{ +} + +BackupJob::~BackupJob() +{ + mPendingFolders.clear(); + if ( mArchive ) { + delete mArchive; + mArchive = 0; + } +} + +void BackupJob::setRootFolder( KMFolder *rootFolder ) +{ + mRootFolder = rootFolder; +} + +void BackupJob::setSaveLocation( const KURL &savePath ) +{ + mMailArchivePath = savePath; +} + +void BackupJob::setArchiveType( ArchiveType type ) +{ + mArchiveType = type; +} + +void BackupJob::setDeleteFoldersAfterCompletion( bool deleteThem ) +{ + mDeleteFoldersAfterCompletion = deleteThem; +} + +TQString BackupJob::stripRootPath( const TQString &path ) const +{ + TQString ret = path; + ret = ret.remove( mRootFolder->path() ); + if ( ret.startsWith( "/" ) ) + ret = ret.right( ret.length() - 1 ); + return ret; +} + +void BackupJob::queueFolders( KMFolder *root ) +{ + mPendingFolders.append( root ); + KMFolderDir *dir = root->child(); + if ( dir ) { + for ( KMFolderNode * node = dir->first() ; node ; node = dir->next() ) { + if ( node->isDir() ) + continue; + KMFolder *folder = static_cast<KMFolder*>( node ); + queueFolders( folder ); + } + } +} + +bool BackupJob::hasChildren( KMFolder *folder ) const +{ + KMFolderDir *dir = folder->child(); + if ( dir ) { + for ( KMFolderNode * node = dir->first() ; node ; node = dir->next() ) { + if ( !node->isDir() ) + return true; + } + } + return false; +} + +void BackupJob::cancelJob() +{ + abort( i18n( "The operation was canceled by the user." ) ); +} + +void BackupJob::abort( const TQString &errorMessage ) +{ + // We could be called this twice, since killing the current job below will cause the job to fail, + // and that will call abort() + if ( mAborted ) + return; + + mAborted = true; + if ( mCurrentFolderOpen && mCurrentFolder ) { + mCurrentFolder->close( "BackupJob" ); + mCurrentFolder = 0; + } + if ( mArchive && mArchive->isOpened() ) { + mArchive->close(); + } + if ( mCurrentJob ) { + mCurrentJob->kill(); + mCurrentJob = 0; + } + if ( mProgressItem ) { + mProgressItem->setComplete(); + mProgressItem = 0; + // The progressmanager will delete it + } + + TQString text = i18n( "Failed to archive the folder '%1'." ).arg( mRootFolder->name() ); + text += "\n" + errorMessage; + KMessageBox::sorry( mParentWidget, text, i18n( "Archiving failed." ) ); + deleteLater(); + // Clean up archive file here? +} + +void BackupJob::finish() +{ + if ( mArchive->isOpened() ) { + mArchive->close(); + if ( !mArchive->closeSucceeded() ) { + abort( i18n( "Unable to finalize the archive file." ) ); + return; + } + } + + mProgressItem->setStatus( i18n( "Archiving finished" ) ); + mProgressItem->setComplete(); + mProgressItem = 0; + + TQFileInfo archiveFileInfo( mMailArchivePath.path() ); + TQString text = i18n( "Archiving folder '%1' successfully completed. " + "The archive was written to the file '%2'." ) + .arg( mRootFolder->name() ).arg( mMailArchivePath.path() ); + text += "\n" + i18n( "1 message of size %1 was archived.", + "%n messages with the total size of %1 were archived.", mArchivedMessages ) + .arg( KIO::convertSize( mArchivedSize ) ); + text += "\n" + i18n( "The archive file has a size of %1." ) + .arg( KIO::convertSize( archiveFileInfo.size() ) ); + KMessageBox::information( mParentWidget, text, i18n( "Archiving finished." ) ); + + if ( mDeleteFoldersAfterCompletion ) { + // Some saftey checks first... + if ( archiveFileInfo.size() > 0 && ( mArchivedSize > 0 || mArchivedMessages == 0 ) ) { + // Sorry for any data loss! + FolderUtil::deleteFolder( mRootFolder, mParentWidget ); + } + } + + deleteLater(); +} + +void BackupJob::archiveNextMessage() +{ + if ( mAborted ) + return; + + mCurrentMessage = 0; + if ( mPendingMessages.isEmpty() ) { + kdDebug(5006) << "===> All messages done in folder " << mCurrentFolder->name() << endl; + mCurrentFolder->close( "BackupJob" ); + mCurrentFolderOpen = false; + archiveNextFolder(); + return; + } + + unsigned long serNum = mPendingMessages.front(); + mPendingMessages.pop_front(); + + KMFolder *folder; + mMessageIndex = -1; + KMMsgDict::instance()->getLocation( serNum, &folder, &mMessageIndex ); + if ( mMessageIndex == -1 ) { + kdWarning(5006) << "Failed to get message location for sernum " << serNum << endl; + abort( i18n( "Unable to retrieve a message for folder '%1'." ).arg( mCurrentFolder->name() ) ); + return; + } + + Q_ASSERT( folder == mCurrentFolder ); + const KMMsgBase *base = mCurrentFolder->getMsgBase( mMessageIndex ); + mUnget = base && !base->isMessage(); + KMMessage *message = mCurrentFolder->getMsg( mMessageIndex ); + if ( !message ) { + kdWarning(5006) << "Failed to retrieve message with index " << mMessageIndex << endl; + abort( i18n( "Unable to retrieve a message for folder '%1'." ).arg( mCurrentFolder->name() ) ); + return; + } + + kdDebug(5006) << "Going to get next message with subject " << message->subject() << ", " + << mPendingMessages.size() << " messages left in the folder." << endl; + + if ( message->isComplete() ) { + // Use a singleshot timer, or otherwise we risk ending up in a very big recursion + // for folders that have many messages + mCurrentMessage = message; + TQTimer::singleShot( 0, this, TQT_SLOT( processCurrentMessage() ) ); + } + else if ( message->parent() ) { + mCurrentJob = message->parent()->createJob( message ); + mCurrentJob->setCancellable( false ); + connect( mCurrentJob, TQT_SIGNAL( messageRetrieved( KMMessage* ) ), + this, TQT_SLOT( messageRetrieved( KMMessage* ) ) ); + connect( mCurrentJob, TQT_SIGNAL( result( KMail::FolderJob* ) ), + this, TQT_SLOT( folderJobFinished( KMail::FolderJob* ) ) ); + mCurrentJob->start(); + } + else { + kdWarning(5006) << "Message with subject " << mCurrentMessage->subject() + << " is neither complete nor has a parent!" << endl; + abort( i18n( "Internal error while trying to retrieve a message from folder '%1'." ) + .arg( mCurrentFolder->name() ) ); + } + + mProgressItem->setProgress( ( mProgressItem->progress() + 5 ) ); +} + +static int fileInfoToUnixPermissions( const TQFileInfo &fileInfo ) +{ + int perm = 0; + if ( fileInfo.permission( TQFileInfo::ExeOther ) ) perm += S_IXOTH; + if ( fileInfo.permission( TQFileInfo::WriteOther ) ) perm += S_IWOTH; + if ( fileInfo.permission( TQFileInfo::ReadOther ) ) perm += S_IROTH; + if ( fileInfo.permission( TQFileInfo::ExeGroup ) ) perm += S_IXGRP; + if ( fileInfo.permission( TQFileInfo::WriteGroup ) ) perm += S_IWGRP; + if ( fileInfo.permission( TQFileInfo::ReadGroup ) ) perm += S_IRGRP; + if ( fileInfo.permission( TQFileInfo::ExeOwner ) ) perm += S_IXUSR; + if ( fileInfo.permission( TQFileInfo::WriteOwner ) ) perm += S_IWUSR; + if ( fileInfo.permission( TQFileInfo::ReadOwner ) ) perm += S_IRUSR; + return perm; +} + +void BackupJob::processCurrentMessage() +{ + if ( mAborted ) + return; + + if ( mCurrentMessage ) { + kdDebug(5006) << "Processing message with subject " << mCurrentMessage->subject() << endl; + const DwString &messageDWString = mCurrentMessage->asDwString(); + const uint messageSize = messageDWString.size(); + const char *messageString = mCurrentMessage->asDwString().c_str(); + TQString messageName; + TQFileInfo fileInfo; + if ( messageName.isEmpty() ) { + messageName = TQString::number( mCurrentMessage->getMsgSerNum() ); // IMAP doesn't have filenames + if ( mCurrentMessage->storage() ) { + fileInfo.setFile( mCurrentMessage->storage()->location() ); + // TODO: what permissions etc to take when there is no storage file? + } + } + else { + // TODO: What if the message is not in the "cur" directory? + fileInfo.setFile( mCurrentFolder->location() + "/cur/" + mCurrentMessage->fileName() ); + messageName = mCurrentMessage->fileName(); + } + + const TQString fileName = stripRootPath( mCurrentFolder->location() ) + + "/cur/" + messageName; + + TQString user; + TQString group; + mode_t permissions = 0700; + time_t creationTime = time( 0 ); + time_t modificationTime = time( 0 ); + time_t accessTime = time( 0 ); + if ( !fileInfo.fileName().isEmpty() ) { + user = fileInfo.owner(); + group = fileInfo.group(); + permissions = fileInfoToUnixPermissions( fileInfo ); + creationTime = fileInfo.created().toTime_t(); + modificationTime = fileInfo.lastModified().toTime_t(); + accessTime = fileInfo.lastRead().toTime_t(); + } + else { + kdWarning(5006) << "Unable to find file for message " << fileName << endl; + } + + if ( !mArchive->writeFile( fileName, user, group, messageSize, permissions, accessTime, + modificationTime, creationTime, messageString ) ) { + abort( i18n( "Failed to write a message into the archive folder '%1'." ).arg( mCurrentFolder->name() ) ); + return; + } + + if ( mUnget ) { + Q_ASSERT( mMessageIndex >= 0 ); + mCurrentFolder->unGetMsg( mMessageIndex ); + } + + mArchivedMessages++; + mArchivedSize += messageSize; + } + else { + // No message? According to ImapJob::slotGetMessageResult(), that means the message is no + // longer on the server. So ignore this one. + kdWarning(5006) << "Unable to download a message for folder " << mCurrentFolder->name() << endl; + } + archiveNextMessage(); +} + +void BackupJob::messageRetrieved( KMMessage *message ) +{ + mCurrentMessage = message; + processCurrentMessage(); +} + +void BackupJob::folderJobFinished( KMail::FolderJob *job ) +{ + if ( mAborted ) + return; + + // The job might finish after it has emitted messageRetrieved(), in which case we have already + // started a new job. Don't set the current job to 0 in that case. + if ( job == mCurrentJob ) { + mCurrentJob = 0; + } + + if ( job->error() ) { + if ( mCurrentFolder ) + abort( i18n( "Downloading a message in folder '%1' failed." ).arg( mCurrentFolder->name() ) ); + else + abort( i18n( "Downloading a message in the current folder failed." ) ); + } +} + +bool BackupJob::writeDirHelper( const TQString &directoryPath, const TQString &permissionPath ) +{ + TQFileInfo fileInfo( permissionPath ); + TQString user = fileInfo.owner(); + TQString group = fileInfo.group(); + mode_t permissions = fileInfoToUnixPermissions( fileInfo ); + time_t creationTime = fileInfo.created().toTime_t(); + time_t modificationTime = fileInfo.lastModified().toTime_t(); + time_t accessTime = fileInfo.lastRead().toTime_t(); + return mArchive->writeDir( stripRootPath( directoryPath ), user, group, permissions, accessTime, + modificationTime, creationTime ); +} + +void BackupJob::archiveNextFolder() +{ + if ( mAborted ) + return; + + if ( mPendingFolders.isEmpty() ) { + finish(); + return; + } + + mCurrentFolder = mPendingFolders.take( 0 ); + kdDebug(5006) << "===> Archiving next folder: " << mCurrentFolder->name() << endl; + mProgressItem->setStatus( i18n( "Archiving folder %1" ).arg( mCurrentFolder->name() ) ); + if ( mCurrentFolder->open( "BackupJob" ) != 0 ) { + abort( i18n( "Unable to open folder '%1'.").arg( mCurrentFolder->name() ) ); + return; + } + mCurrentFolderOpen = true; + + const TQString folderName = mCurrentFolder->name(); + bool success = true; + if ( hasChildren( mCurrentFolder ) ) { + if ( !writeDirHelper( mCurrentFolder->subdirLocation(), mCurrentFolder->subdirLocation() ) ) + success = false; + } + if ( !writeDirHelper( mCurrentFolder->location(), mCurrentFolder->location() ) ) + success = false; + if ( !writeDirHelper( mCurrentFolder->location() + "/cur", mCurrentFolder->location() ) ) + success = false; + if ( !writeDirHelper( mCurrentFolder->location() + "/new", mCurrentFolder->location() ) ) + success = false; + if ( !writeDirHelper( mCurrentFolder->location() + "/tmp", mCurrentFolder->location() ) ) + success = false; + if ( !success ) { + abort( i18n( "Unable to create folder structure for folder '%1' within archive file." ) + .arg( mCurrentFolder->name() ) ); + return; + } + + for ( int i = 0; i < mCurrentFolder->count( false /* no cache */ ); i++ ) { + unsigned long serNum = KMMsgDict::instance()->getMsgSerNum( mCurrentFolder, i ); + if ( serNum == 0 ) { + // Uh oh + kdWarning(5006) << "Got serial number zero in " << mCurrentFolder->name() + << " at index " << i << "!" << endl; + // TODO: handle error in a nicer way. this is _very_ bad + abort( i18n( "Unable to backup messages in folder '%1', the index file is corrupted." ) + .arg( mCurrentFolder->name() ) ); + return; + } + else + mPendingMessages.append( serNum ); + } + archiveNextMessage(); +} + +// TODO +// - error handling +// - import +// - connect to progressmanager, especially abort +// - messagebox when finished (?) +// - ui dialog +// - use correct permissions +// - save index and serial number? +// - guarded pointers for folders +// - online IMAP: check mails first, so sernums are up-to-date? +// - "ignore errors"-mode, with summary how many messages couldn't be archived? +// - do something when the user quits KMail while the backup job is running +// - run in a thread? +// - delete source folder after completion. dangerous!!! +// +// BUGS +// - Online IMAP: Test Mails -> Test%20Mails +// - corrupted sernums indices stop backup job +void BackupJob::start() +{ + Q_ASSERT( !mMailArchivePath.isEmpty() ); + Q_ASSERT( mRootFolder ); + + queueFolders( mRootFolder ); + + switch ( mArchiveType ) { + case Zip: { + KZip *zip = new KZip( mMailArchivePath.path() ); + zip->setCompression( KZip::DeflateCompression ); + mArchive = zip; + break; + } + case Tar: { + mArchive = new KTar( mMailArchivePath.path(), "application/x-tar" ); + break; + } + case TarGz: { + mArchive = new KTar( mMailArchivePath.path(), "application/x-gzip" ); + break; + } + case TarBz2: { + mArchive = new KTar( mMailArchivePath.path(), "application/x-bzip2" ); + break; + } + } + + kdDebug(5006) << "Starting backup." << endl; + if ( !mArchive->open( IO_WriteOnly ) ) { + abort( i18n( "Unable to open archive for writing." ) ); + return; + } + + mProgressItem = KPIM::ProgressManager::createProgressItem( + "BackupJob", + i18n( "Archiving" ), + TQString(), + true ); + mProgressItem->setUsesBusyIndicator( true ); + connect( mProgressItem, TQT_SIGNAL(progressItemCanceled(KPIM::ProgressItem*)), + this, TQT_SLOT(cancelJob()) ); + + archiveNextFolder(); +} + +#include "backupjob.moc" + |