kmail

backupjob.cpp

00001 /* Copyright 2009 Klarälvdalens Datakonsult AB
00002 
00003    This program is free software; you can redistribute it and/or
00004    modify it under the terms of the GNU General Public License as
00005    published by the Free Software Foundation; either version 2 of
00006    the License or (at your option) version 3 or any later version
00007    accepted by the membership of KDE e.V. (or its successor approved
00008    by the membership of KDE e.V.), which shall act as a proxy
00009    defined in Section 14 of version 3 of the license.
00010 
00011    This program is distributed in the hope that it will be useful,
00012    but WITHOUT ANY WARRANTY; without even the implied warranty of
00013    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00014    GNU General Public License for more details.
00015 
00016    You should have received a copy of the GNU General Public License
00017    along with this program.  If not, see <http://www.gnu.org/licenses/>.
00018 */
00019 #include "backupjob.h"
00020 
00021 #include "kmmsgdict.h"
00022 #include "kmfolder.h"
00023 #include "kmfoldercachedimap.h"
00024 #include "kmfolderdir.h"
00025 #include "folderutil.h"
00026 
00027 #include "progressmanager.h"
00028 
00029 #include "kzip.h"
00030 #include "ktar.h"
00031 #include "kmessagebox.h"
00032 
00033 #include "qfile.h"
00034 #include "qfileinfo.h"
00035 #include "qstringlist.h"
00036 
00037 using namespace KMail;
00038 
00039 BackupJob::BackupJob( QWidget *parent )
00040   : QObject( parent ),
00041     mArchiveType( Zip ),
00042     mRootFolder( 0 ),
00043     mArchive( 0 ),
00044     mParentWidget( parent ),
00045     mCurrentFolderOpen( false ),
00046     mArchivedMessages( 0 ),
00047     mArchivedSize( 0 ),
00048     mProgressItem( 0 ),
00049     mAborted( false ),
00050     mDeleteFoldersAfterCompletion( false ),
00051     mCurrentFolder( 0 ),
00052     mCurrentMessage( 0 ),
00053     mCurrentJob( 0 )
00054 {
00055 }
00056 
00057 BackupJob::~BackupJob()
00058 {
00059   mPendingFolders.clear();
00060   if ( mArchive ) {
00061     delete mArchive;
00062     mArchive = 0;
00063   }
00064 }
00065 
00066 void BackupJob::setRootFolder( KMFolder *rootFolder )
00067 {
00068   mRootFolder = rootFolder;
00069 }
00070 
00071 void BackupJob::setSaveLocation( const KURL &savePath )
00072 {
00073   mMailArchivePath = savePath;
00074 }
00075 
00076 void BackupJob::setArchiveType( ArchiveType type )
00077 {
00078   mArchiveType = type;
00079 }
00080 
00081 void BackupJob::setDeleteFoldersAfterCompletion( bool deleteThem )
00082 {
00083   mDeleteFoldersAfterCompletion = deleteThem;
00084 }
00085 
00086 QString BackupJob::stripRootPath( const QString &path ) const
00087 {
00088   QString ret = path;
00089   ret = ret.remove( mRootFolder->path() );
00090   if ( ret.startsWith( "/" ) )
00091     ret = ret.right( ret.length() - 1 );
00092   return ret;
00093 }
00094 
00095 void BackupJob::queueFolders( KMFolder *root )
00096 {
00097   mPendingFolders.append( root );
00098   KMFolderDir *dir = root->child();
00099   if ( dir ) {
00100     for ( KMFolderNode * node = dir->first() ; node ; node = dir->next() ) {
00101       if ( node->isDir() )
00102         continue;
00103       KMFolder *folder = static_cast<KMFolder*>( node );
00104       queueFolders( folder );
00105     }
00106   }
00107 }
00108 
00109 bool BackupJob::hasChildren( KMFolder *folder ) const
00110 {
00111   KMFolderDir *dir = folder->child();
00112   if ( dir ) {
00113     for ( KMFolderNode * node = dir->first() ; node ; node = dir->next() ) {
00114       if ( !node->isDir() )
00115         return true;
00116     }
00117   }
00118   return false;
00119 }
00120 
00121 void BackupJob::cancelJob()
00122 {
00123   abort( i18n( "The operation was canceled by the user." ) );
00124 }
00125 
00126 void BackupJob::abort( const QString &errorMessage )
00127 {
00128   // We could be called this twice, since killing the current job below will cause the job to fail,
00129   // and that will call abort()
00130   if ( mAborted )
00131     return;
00132 
00133   mAborted = true;
00134   if ( mCurrentFolderOpen && mCurrentFolder ) {
00135     mCurrentFolder->close( "BackupJob" );
00136     mCurrentFolder = 0;
00137   }
00138   if ( mArchive && mArchive->isOpened() ) {
00139     mArchive->close();
00140   }
00141   if ( mCurrentJob ) {
00142     mCurrentJob->kill();
00143     mCurrentJob = 0;
00144   }
00145   if ( mProgressItem ) {
00146     mProgressItem->setComplete();
00147     mProgressItem = 0;
00148     // The progressmanager will delete it
00149   }
00150 
00151   QString text = i18n( "Failed to archive the folder '%1'." ).arg( mRootFolder->name() );
00152   text += "\n" + errorMessage;
00153   KMessageBox::sorry( mParentWidget, text, i18n( "Archiving failed." ) );
00154   deleteLater();
00155   // Clean up archive file here?
00156 }
00157 
00158 void BackupJob::finish()
00159 {
00160   if ( mArchive->isOpened() ) {
00161     mArchive->close();
00162     if ( !mArchive->closeSucceeded() ) {
00163       abort( i18n( "Unable to finalize the archive file." ) );
00164       return;
00165     }
00166   }
00167 
00168   mProgressItem->setStatus( i18n( "Archiving finished" ) );
00169   mProgressItem->setComplete();
00170   mProgressItem = 0;
00171 
00172   QFileInfo archiveFileInfo( mMailArchivePath.path() );
00173   QString text = i18n( "Archiving folder '%1' successfully completed. "
00174                        "The archive was written to the file '%2'." )
00175                    .arg( mRootFolder->name() ).arg( mMailArchivePath.path() );
00176   text += "\n" + i18n( "1 message of size %1 was archived.",
00177                        "%n messages with the total size of %1 were archived.", mArchivedMessages )
00178                    .arg( KIO::convertSize( mArchivedSize ) );
00179   text += "\n" + i18n( "The archive file has a size of %1." )
00180                    .arg( KIO::convertSize( archiveFileInfo.size() ) );
00181   KMessageBox::information( mParentWidget, text, i18n( "Archiving finished." ) );
00182 
00183   if ( mDeleteFoldersAfterCompletion ) {
00184     // Some saftey checks first...
00185     if ( archiveFileInfo.size() > 0 && ( mArchivedSize > 0 || mArchivedMessages == 0 ) ) {
00186       // Sorry for any data loss!
00187       FolderUtil::deleteFolder( mRootFolder, mParentWidget );
00188     }
00189   }
00190 
00191   deleteLater();
00192 }
00193 
00194 void BackupJob::archiveNextMessage()
00195 {
00196   if ( mAborted )
00197     return;
00198 
00199   mCurrentMessage = 0;
00200   if ( mPendingMessages.isEmpty() ) {
00201     kdDebug(5006) << "===> All messages done in folder " << mCurrentFolder->name() << endl;
00202     mCurrentFolder->close( "BackupJob" );
00203     mCurrentFolderOpen = false;
00204     archiveNextFolder();
00205     return;
00206   }
00207 
00208   unsigned long serNum = mPendingMessages.front();
00209   mPendingMessages.pop_front();
00210 
00211   KMFolder *folder;
00212   int index = -1;
00213   KMMsgDict::instance()->getLocation( serNum, &folder, &index );
00214   if ( index == -1 ) {
00215     kdWarning(5006) << "Failed to get message location for sernum " << serNum << endl;
00216     abort( i18n( "Unable to retrieve a message for folder '%1'." ).arg( mCurrentFolder->name() ) );
00217     return;
00218   }
00219 
00220   Q_ASSERT( folder == mCurrentFolder );
00221   KMMessage *message = mCurrentFolder->getMsg( index );
00222   if ( !message ) {
00223     kdWarning(5006) << "Failed to retrieve message with index " << index << endl;
00224     abort( i18n( "Unable to retrieve a message for folder '%1'." ).arg( mCurrentFolder->name() ) );
00225     return;
00226   }
00227 
00228   kdDebug(5006) << "Going to get next message with subject " << message->subject() << ", "
00229                 << mPendingMessages.size() << " messages left in the folder." << endl;
00230 
00231   if ( message->isComplete() ) {
00232     // Use a singleshot timer, or otherwise we risk ending up in a very big recursion
00233     // for folders that have many messages
00234     mCurrentMessage = message;
00235     QTimer::singleShot( 0, this, SLOT( processCurrentMessage() ) );
00236   }
00237   else if ( message->parent() ) {
00238     mCurrentJob = message->parent()->createJob( message );
00239     mCurrentJob->setCancellable( false );
00240     connect( mCurrentJob, SIGNAL( messageRetrieved( KMMessage* ) ),
00241              this, SLOT( messageRetrieved( KMMessage* ) ) );
00242     connect( mCurrentJob, SIGNAL( result( KMail::FolderJob* ) ),
00243              this, SLOT( folderJobFinished( KMail::FolderJob* ) ) );
00244     mCurrentJob->start();
00245   }
00246   else {
00247     kdWarning(5006) << "Message with subject " << mCurrentMessage->subject()
00248                     << " is neither complete nor has a parent!" << endl;
00249     abort( i18n( "Internal error while trying to retrieve a message from folder '%1'." )
00250               .arg( mCurrentFolder->name() ) );
00251   }
00252 
00253   mProgressItem->setProgress( ( mProgressItem->progress() + 5 ) );
00254 }
00255 
00256 static int fileInfoToUnixPermissions( const QFileInfo &fileInfo )
00257 {
00258   int perm = 0;
00259   if ( fileInfo.permission( QFileInfo::ExeOther ) ) perm += S_IXOTH;
00260   if ( fileInfo.permission( QFileInfo::WriteOther ) ) perm += S_IWOTH;
00261   if ( fileInfo.permission( QFileInfo::ReadOther ) ) perm += S_IROTH;
00262   if ( fileInfo.permission( QFileInfo::ExeGroup ) ) perm += S_IXGRP;
00263   if ( fileInfo.permission( QFileInfo::WriteGroup ) ) perm += S_IWGRP;
00264   if ( fileInfo.permission( QFileInfo::ReadGroup ) ) perm += S_IRGRP;
00265   if ( fileInfo.permission( QFileInfo::ExeOwner ) ) perm += S_IXUSR;
00266   if ( fileInfo.permission( QFileInfo::WriteOwner ) ) perm += S_IWUSR;
00267   if ( fileInfo.permission( QFileInfo::ReadOwner ) ) perm += S_IRUSR;
00268   return perm;
00269 }
00270 
00271 void BackupJob::processCurrentMessage()
00272 {
00273   if ( mAborted )
00274     return;
00275 
00276   if ( mCurrentMessage ) {
00277     kdDebug(5006) << "Processing message with subject " << mCurrentMessage->subject() << endl;
00278     const DwString &messageDWString = mCurrentMessage->asDwString();
00279     const uint messageSize = messageDWString.size();
00280     const char *messageString = mCurrentMessage->asDwString().c_str();
00281     QString messageName;
00282     QFileInfo fileInfo;
00283     if ( messageName.isEmpty() ) {
00284       messageName = QString::number( mCurrentMessage->getMsgSerNum() ); // IMAP doesn't have filenames
00285       if ( mCurrentMessage->storage() ) {
00286         fileInfo.setFile( mCurrentMessage->storage()->location() );
00287         // TODO: what permissions etc to take when there is no storage file?
00288       }
00289     }
00290     else {
00291       // TODO: What if the message is not in the "cur" directory?
00292       fileInfo.setFile( mCurrentFolder->location() + "/cur/" + mCurrentMessage->fileName() );
00293       messageName = mCurrentMessage->fileName();
00294     }
00295 
00296     const QString fileName = stripRootPath( mCurrentFolder->location() ) +
00297                              "/cur/" + messageName;
00298 
00299     QString user;
00300     QString group;
00301     mode_t permissions = 0700;
00302     time_t creationTime = time( 0 );
00303     time_t modificationTime = time( 0 );
00304     time_t accessTime = time( 0 );
00305     if ( !fileInfo.fileName().isEmpty() ) {
00306       user = fileInfo.owner();
00307       group = fileInfo.group();
00308       permissions = fileInfoToUnixPermissions( fileInfo );
00309       creationTime = fileInfo.created().toTime_t();
00310       modificationTime = fileInfo.lastModified().toTime_t();
00311       accessTime = fileInfo.lastRead().toTime_t();
00312     }
00313     else {
00314       kdWarning(5006) << "Unable to find file for message " << fileName << endl;
00315     }
00316 
00317     if ( !mArchive->writeFile( fileName, user, group, messageSize, permissions, accessTime,
00318                                modificationTime, creationTime, messageString ) ) {
00319       abort( i18n( "Failed to write a message into the archive folder '%1'." ).arg( mCurrentFolder->name() ) );
00320       return;
00321     }
00322 
00323     mArchivedMessages++;
00324     mArchivedSize += messageSize;
00325   }
00326   else {
00327     // No message? According to ImapJob::slotGetMessageResult(), that means the message is no
00328     // longer on the server. So ignore this one.
00329     kdWarning(5006) << "Unable to download a message for folder " << mCurrentFolder->name() << endl;
00330   }
00331   archiveNextMessage();
00332 }
00333 
00334 void BackupJob::messageRetrieved( KMMessage *message )
00335 {
00336   mCurrentMessage = message;
00337   processCurrentMessage();
00338 }
00339 
00340 void BackupJob::folderJobFinished( KMail::FolderJob *job )
00341 {
00342   if ( mAborted )
00343     return;
00344 
00345   // The job might finish after it has emitted messageRetrieved(), in which case we have already
00346   // started a new job. Don't set the current job to 0 in that case.
00347   if ( job == mCurrentJob ) {
00348     mCurrentJob = 0;
00349   }
00350 
00351   if ( job->error() ) {
00352     if ( mCurrentFolder )
00353       abort( i18n( "Downloading a message in folder '%1' failed." ).arg( mCurrentFolder->name() ) );
00354     else
00355       abort( i18n( "Downloading a message in the current folder failed." ) );
00356   }
00357 }
00358 
00359 bool BackupJob::writeDirHelper( const QString &directoryPath, const QString &permissionPath )
00360 {
00361   QFileInfo fileInfo( permissionPath );
00362   QString user = fileInfo.owner();
00363   QString group = fileInfo.group();
00364   mode_t permissions = fileInfoToUnixPermissions( fileInfo );
00365   time_t creationTime = fileInfo.created().toTime_t();
00366   time_t modificationTime = fileInfo.lastModified().toTime_t();
00367   time_t accessTime = fileInfo.lastRead().toTime_t();
00368   return mArchive->writeDir( stripRootPath( directoryPath ), user, group, permissions, accessTime,
00369                              modificationTime, creationTime );
00370 }
00371 
00372 void BackupJob::archiveNextFolder()
00373 {
00374   if ( mAborted )
00375     return;
00376 
00377   if ( mPendingFolders.isEmpty() ) {
00378     finish();
00379     return;
00380   }
00381 
00382   mCurrentFolder = mPendingFolders.take( 0 );
00383   kdDebug(5006) << "===> Archiving next folder: " << mCurrentFolder->name() << endl;
00384   mProgressItem->setStatus( i18n( "Archiving folder %1" ).arg( mCurrentFolder->name() ) );
00385   if ( mCurrentFolder->open( "BackupJob" ) != 0 ) {
00386     abort( i18n( "Unable to open folder '%1'.").arg( mCurrentFolder->name() ) );
00387     return;
00388   }
00389   mCurrentFolderOpen = true;
00390 
00391   const QString folderName = mCurrentFolder->name();
00392   bool success = true;
00393   if ( hasChildren( mCurrentFolder ) ) {
00394     if ( !writeDirHelper( mCurrentFolder->subdirLocation(), mCurrentFolder->subdirLocation() ) )
00395       success = false;
00396   }
00397   if ( !writeDirHelper( mCurrentFolder->location(), mCurrentFolder->location() ) )
00398     success = false;
00399   if ( !writeDirHelper( mCurrentFolder->location() + "/cur", mCurrentFolder->location() ) )
00400     success = false;
00401   if ( !writeDirHelper( mCurrentFolder->location() + "/new", mCurrentFolder->location() ) )
00402     success = false;
00403   if ( !writeDirHelper( mCurrentFolder->location() + "/tmp", mCurrentFolder->location() ) )
00404     success = false;
00405   if ( !success ) {
00406     abort( i18n( "Unable to create folder structure for folder '%1' within archive file." )
00407               .arg( mCurrentFolder->name() ) );
00408     return;
00409   }
00410 
00411   for ( int i = 0; i < mCurrentFolder->count( false /* no cache */ ); i++ ) {
00412     unsigned long serNum = KMMsgDict::instance()->getMsgSerNum( mCurrentFolder, i );
00413     if ( serNum == 0 ) {
00414       // Uh oh
00415       kdWarning(5006) << "Got serial number zero in " << mCurrentFolder->name()
00416                       << " at index " << i << "!" << endl;
00417       // TODO: handle error in a nicer way. this is _very_ bad
00418       abort( i18n( "Unable to backup messages in folder '%1', the index file is corrupted." )
00419                .arg( mCurrentFolder->name() ) );
00420       return;
00421     }
00422     else
00423       mPendingMessages.append( serNum );
00424   }
00425   archiveNextMessage();
00426 }
00427 
00428 // TODO
00429 // - error handling
00430 // - import
00431 // - connect to progressmanager, especially abort
00432 // - messagebox when finished (?)
00433 // - ui dialog
00434 // - use correct permissions
00435 // - save index and serial number?
00436 // - guarded pointers for folders
00437 // - online IMAP: check mails first, so sernums are up-to-date?
00438 // - "ignore errors"-mode, with summary how many messages couldn't be archived?
00439 // - do something when the user quits KMail while the backup job is running
00440 // - run in a thread?
00441 // - delete source folder after completion. dangerous!!!
00442 //
00443 // BUGS
00444 // - Online IMAP: Test Mails -> Test%20Mails
00445 // - corrupted sernums indices stop backup job
00446 void BackupJob::start()
00447 {
00448   Q_ASSERT( !mMailArchivePath.isEmpty() );
00449   Q_ASSERT( mRootFolder );
00450 
00451   queueFolders( mRootFolder );
00452 
00453   switch ( mArchiveType ) {
00454     case Zip: {
00455       KZip *zip = new KZip( mMailArchivePath.path() );
00456       zip->setCompression( KZip::DeflateCompression );
00457       mArchive = zip;
00458       break;
00459     }
00460     case Tar: {
00461       mArchive = new KTar( mMailArchivePath.path(), "application/x-tar" );
00462       break;
00463     }
00464     case TarGz: {
00465       mArchive = new KTar( mMailArchivePath.path(), "application/x-gzip" );
00466       break;
00467     }
00468     case TarBz2: {
00469       mArchive = new KTar( mMailArchivePath.path(), "application/x-bzip2" );
00470       break;
00471     }
00472   }
00473 
00474   kdDebug(5006) << "Starting backup." << endl;
00475   if ( !mArchive->open( IO_WriteOnly ) ) {
00476     abort( i18n( "Unable to open archive for writing." ) );
00477     return;
00478   }
00479 
00480   mProgressItem = KPIM::ProgressManager::createProgressItem(
00481       "BackupJob",
00482       i18n( "Archiving" ),
00483       QString(),
00484       true );
00485   mProgressItem->setUsesBusyIndicator( true );
00486   connect( mProgressItem, SIGNAL(progressItemCanceled(KPIM::ProgressItem*)),
00487            this, SLOT(cancelJob()) );
00488 
00489   archiveNextFolder();
00490 }
00491 
00492 #include "backupjob.moc"
00493