kmail

backupjob.cpp
1 /* Copyright 2009 Klarälvdalens Datakonsult AB
2 
3  This program is free software; you can redistribute it and/or
4  modify it under the terms of the GNU General Public License as
5  published by the Free Software Foundation; either version 2 of
6  the License or (at your option) version 3 or any later version
7  accepted by the membership of KDE e.V. (or its successor approved
8  by the membership of KDE e.V.), which shall act as a proxy
9  defined in Section 14 of version 3 of the license.
10 
11  This program is distributed in the hope that it will be useful,
12  but WITHOUT ANY WARRANTY; without even the implied warranty of
13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  GNU General Public License for more details.
15 
16  You should have received a copy of the GNU General Public License
17  along with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19 #include "backupjob.h"
20 
21 #include "kmmsgdict.h"
22 #include "kmfolder.h"
23 #include "kmfoldercachedimap.h"
24 #include "kmfolderdir.h"
25 #include "folderutil.h"
26 
27 #include "progressmanager.h"
28 
29 #include "kzip.h"
30 #include "ktar.h"
31 #include "tdemessagebox.h"
32 
33 #include "tqfile.h"
34 #include "tqfileinfo.h"
35 #include "tqstringlist.h"
36 
37 using namespace KMail;
38 
39 BackupJob::BackupJob( TQWidget *parent )
40  : TQObject( parent ),
41  mArchiveType( Zip ),
42  mRootFolder( 0 ),
43  mArchive( 0 ),
44  mParentWidget( parent ),
45  mCurrentFolderOpen( false ),
46  mArchivedMessages( 0 ),
47  mArchivedSize( 0 ),
48  mProgressItem( 0 ),
49  mAborted( false ),
50  mDeleteFoldersAfterCompletion( false ),
51  mCurrentFolder( 0 ),
52  mCurrentMessage( 0 ),
53  mCurrentJob( 0 )
54 {
55 }
56 
57 BackupJob::~BackupJob()
58 {
59  mPendingFolders.clear();
60  if ( mArchive ) {
61  delete mArchive;
62  mArchive = 0;
63  }
64 }
65 
66 void BackupJob::setRootFolder( KMFolder *rootFolder )
67 {
68  mRootFolder = rootFolder;
69 }
70 
71 void BackupJob::setSaveLocation( const KURL &savePath )
72 {
73  mMailArchivePath = savePath;
74 }
75 
76 void BackupJob::setArchiveType( ArchiveType type )
77 {
78  mArchiveType = type;
79 }
80 
81 void BackupJob::setDeleteFoldersAfterCompletion( bool deleteThem )
82 {
83  mDeleteFoldersAfterCompletion = deleteThem;
84 }
85 
86 TQString BackupJob::stripRootPath( const TQString &path ) const
87 {
88  TQString ret = path;
89  ret = ret.remove( mRootFolder->path() );
90  if ( ret.startsWith( "/" ) )
91  ret = ret.right( ret.length() - 1 );
92  return ret;
93 }
94 
95 void BackupJob::queueFolders( KMFolder *root )
96 {
97  mPendingFolders.append( root );
98  KMFolderDir *dir = root->child();
99  if ( dir ) {
100  for ( KMFolderNode * node = dir->first() ; node ; node = dir->next() ) {
101  if ( node->isDir() )
102  continue;
103  KMFolder *folder = static_cast<KMFolder*>( node );
104  queueFolders( folder );
105  }
106  }
107 }
108 
109 bool BackupJob::hasChildren( KMFolder *folder ) const
110 {
111  KMFolderDir *dir = folder->child();
112  if ( dir ) {
113  for ( KMFolderNode * node = dir->first() ; node ; node = dir->next() ) {
114  if ( !node->isDir() )
115  return true;
116  }
117  }
118  return false;
119 }
120 
121 void BackupJob::cancelJob()
122 {
123  abort( i18n( "The operation was canceled by the user." ) );
124 }
125 
126 void BackupJob::abort( const TQString &errorMessage )
127 {
128  // We could be called this twice, since killing the current job below will cause the job to fail,
129  // and that will call abort()
130  if ( mAborted )
131  return;
132 
133  mAborted = true;
134  if ( mCurrentFolderOpen && mCurrentFolder ) {
135  mCurrentFolder->close( "BackupJob" );
136  mCurrentFolder = 0;
137  }
138  if ( mArchive && mArchive->isOpened() ) {
139  mArchive->close();
140  }
141  if ( mCurrentJob ) {
142  mCurrentJob->kill();
143  mCurrentJob = 0;
144  }
145  if ( mProgressItem ) {
146  mProgressItem->setComplete();
147  mProgressItem = 0;
148  // The progressmanager will delete it
149  }
150 
151  TQString text = i18n( "Failed to archive the folder '%1'." ).arg( mRootFolder->name() );
152  text += "\n" + errorMessage;
153  KMessageBox::sorry( mParentWidget, text, i18n( "Archiving failed." ) );
154  deleteLater();
155  // Clean up archive file here?
156 }
157 
158 void BackupJob::finish()
159 {
160  if ( mArchive->isOpened() ) {
161  mArchive->close();
162  if ( !mArchive->closeSucceeded() ) {
163  abort( i18n( "Unable to finalize the archive file." ) );
164  return;
165  }
166  }
167 
168  mProgressItem->setStatus( i18n( "Archiving finished" ) );
169  mProgressItem->setComplete();
170  mProgressItem = 0;
171 
172  TQFileInfo archiveFileInfo( mMailArchivePath.path() );
173  TQString text = i18n( "Archiving folder '%1' successfully completed. "
174  "The archive was written to the file '%2'." )
175  .arg( mRootFolder->name() ).arg( mMailArchivePath.path() );
176  text += "\n" + i18n( "1 message of size %1 was archived.",
177  "%n messages with the total size of %1 were archived.", mArchivedMessages )
178  .arg( TDEIO::convertSize( mArchivedSize ) );
179  text += "\n" + i18n( "The archive file has a size of %1." )
180  .arg( TDEIO::convertSize( archiveFileInfo.size() ) );
181  KMessageBox::information( mParentWidget, text, i18n( "Archiving finished." ) );
182 
183  if ( mDeleteFoldersAfterCompletion ) {
184  // Some saftey checks first...
185  if ( archiveFileInfo.size() > 0 && ( mArchivedSize > 0 || mArchivedMessages == 0 ) ) {
186  // Sorry for any data loss!
187  FolderUtil::deleteFolder( mRootFolder, mParentWidget );
188  }
189  }
190 
191  deleteLater();
192 }
193 
194 void BackupJob::archiveNextMessage()
195 {
196  if ( mAborted )
197  return;
198 
199  mCurrentMessage = 0;
200  if ( mPendingMessages.isEmpty() ) {
201  kdDebug(5006) << "===> All messages done in folder " << mCurrentFolder->name() << endl;
202  mCurrentFolder->close( "BackupJob" );
203  mCurrentFolderOpen = false;
204  archiveNextFolder();
205  return;
206  }
207 
208  unsigned long serNum = mPendingMessages.front();
209  mPendingMessages.pop_front();
210 
211  KMFolder *folder;
212  mMessageIndex = -1;
213  KMMsgDict::instance()->getLocation( serNum, &folder, &mMessageIndex );
214  if ( mMessageIndex == -1 ) {
215  kdWarning(5006) << "Failed to get message location for sernum " << serNum << endl;
216  abort( i18n( "Unable to retrieve a message for folder '%1'." ).arg( mCurrentFolder->name() ) );
217  return;
218  }
219 
220  Q_ASSERT( folder == mCurrentFolder );
221  const KMMsgBase *base = mCurrentFolder->getMsgBase( mMessageIndex );
222  mUnget = base && !base->isMessage();
223  KMMessage *message = mCurrentFolder->getMsg( mMessageIndex );
224  if ( !message ) {
225  kdWarning(5006) << "Failed to retrieve message with index " << mMessageIndex << endl;
226  abort( i18n( "Unable to retrieve a message for folder '%1'." ).arg( mCurrentFolder->name() ) );
227  return;
228  }
229 
230  kdDebug(5006) << "Going to get next message with subject " << message->subject() << ", "
231  << mPendingMessages.size() << " messages left in the folder." << endl;
232 
233  if ( message->isComplete() ) {
234  // Use a singleshot timer, or otherwise we risk ending up in a very big recursion
235  // for folders that have many messages
236  mCurrentMessage = message;
237  TQTimer::singleShot( 0, this, TQ_SLOT( processCurrentMessage() ) );
238  }
239  else if ( message->parent() ) {
240  mCurrentJob = message->parent()->createJob( message );
241  mCurrentJob->setCancellable( false );
242  connect( mCurrentJob, TQ_SIGNAL( messageRetrieved( KMMessage* ) ),
243  this, TQ_SLOT( messageRetrieved( KMMessage* ) ) );
244  connect( mCurrentJob, TQ_SIGNAL( result( KMail::FolderJob* ) ),
245  this, TQ_SLOT( folderJobFinished( KMail::FolderJob* ) ) );
246  mCurrentJob->start();
247  }
248  else {
249  kdWarning(5006) << "Message with subject " << mCurrentMessage->subject()
250  << " is neither complete nor has a parent!" << endl;
251  abort( i18n( "Internal error while trying to retrieve a message from folder '%1'." )
252  .arg( mCurrentFolder->name() ) );
253  }
254 
255  mProgressItem->setProgress( ( mProgressItem->progress() + 5 ) );
256 }
257 
258 static int fileInfoToUnixPermissions( const TQFileInfo &fileInfo )
259 {
260  int perm = 0;
261  if ( fileInfo.permission( TQFileInfo::ExeOther ) ) perm += S_IXOTH;
262  if ( fileInfo.permission( TQFileInfo::WriteOther ) ) perm += S_IWOTH;
263  if ( fileInfo.permission( TQFileInfo::ReadOther ) ) perm += S_IROTH;
264  if ( fileInfo.permission( TQFileInfo::ExeGroup ) ) perm += S_IXGRP;
265  if ( fileInfo.permission( TQFileInfo::WriteGroup ) ) perm += S_IWGRP;
266  if ( fileInfo.permission( TQFileInfo::ReadGroup ) ) perm += S_IRGRP;
267  if ( fileInfo.permission( TQFileInfo::ExeOwner ) ) perm += S_IXUSR;
268  if ( fileInfo.permission( TQFileInfo::WriteOwner ) ) perm += S_IWUSR;
269  if ( fileInfo.permission( TQFileInfo::ReadOwner ) ) perm += S_IRUSR;
270  return perm;
271 }
272 
273 void BackupJob::processCurrentMessage()
274 {
275  if ( mAborted )
276  return;
277 
278  if ( mCurrentMessage ) {
279  kdDebug(5006) << "Processing message with subject " << mCurrentMessage->subject() << endl;
280  const DwString &messageDWString = mCurrentMessage->asDwString();
281  const uint messageSize = messageDWString.size();
282  const char *messageString = mCurrentMessage->asDwString().c_str();
283  TQString messageName;
284  TQFileInfo fileInfo;
285  if ( messageName.isEmpty() ) {
286  messageName = TQString::number( mCurrentMessage->getMsgSerNum() ); // IMAP doesn't have filenames
287  if ( mCurrentMessage->storage() ) {
288  fileInfo.setFile( mCurrentMessage->storage()->location() );
289  // TODO: what permissions etc to take when there is no storage file?
290  }
291  }
292  else {
293  // TODO: What if the message is not in the "cur" directory?
294  fileInfo.setFile( mCurrentFolder->location() + "/cur/" + mCurrentMessage->fileName() );
295  messageName = mCurrentMessage->fileName();
296  }
297 
298  const TQString fileName = stripRootPath( mCurrentFolder->location() ) +
299  "/cur/" + messageName;
300 
301  TQString user;
302  TQString group;
303  mode_t permissions = 0700;
304  time_t creationTime = time( 0 );
305  time_t modificationTime = time( 0 );
306  time_t accessTime = time( 0 );
307  if ( !fileInfo.fileName().isEmpty() ) {
308  user = fileInfo.owner();
309  group = fileInfo.group();
310  permissions = fileInfoToUnixPermissions( fileInfo );
311  creationTime = fileInfo.created().toTime_t();
312  modificationTime = fileInfo.lastModified().toTime_t();
313  accessTime = fileInfo.lastRead().toTime_t();
314  }
315  else {
316  kdWarning(5006) << "Unable to find file for message " << fileName << endl;
317  }
318 
319  if ( !mArchive->writeFile( fileName, user, group, messageSize, permissions, accessTime,
320  modificationTime, creationTime, messageString ) ) {
321  abort( i18n( "Failed to write a message into the archive folder '%1'." ).arg( mCurrentFolder->name() ) );
322  return;
323  }
324 
325  if ( mUnget ) {
326  Q_ASSERT( mMessageIndex >= 0 );
327  mCurrentFolder->unGetMsg( mMessageIndex );
328  }
329 
330  mArchivedMessages++;
331  mArchivedSize += messageSize;
332  }
333  else {
334  // No message? According to ImapJob::slotGetMessageResult(), that means the message is no
335  // longer on the server. So ignore this one.
336  kdWarning(5006) << "Unable to download a message for folder " << mCurrentFolder->name() << endl;
337  }
338  archiveNextMessage();
339 }
340 
341 void BackupJob::messageRetrieved( KMMessage *message )
342 {
343  mCurrentMessage = message;
344  processCurrentMessage();
345 }
346 
347 void BackupJob::folderJobFinished( KMail::FolderJob *job )
348 {
349  if ( mAborted )
350  return;
351 
352  // The job might finish after it has emitted messageRetrieved(), in which case we have already
353  // started a new job. Don't set the current job to 0 in that case.
354  if ( job == mCurrentJob ) {
355  mCurrentJob = 0;
356  }
357 
358  if ( job->error() ) {
359  if ( mCurrentFolder )
360  abort( i18n( "Downloading a message in folder '%1' failed." ).arg( mCurrentFolder->name() ) );
361  else
362  abort( i18n( "Downloading a message in the current folder failed." ) );
363  }
364 }
365 
366 bool BackupJob::writeDirHelper( const TQString &directoryPath, const TQString &permissionPath )
367 {
368  TQFileInfo fileInfo( permissionPath );
369  TQString user = fileInfo.owner();
370  TQString group = fileInfo.group();
371  mode_t permissions = fileInfoToUnixPermissions( fileInfo );
372  time_t creationTime = fileInfo.created().toTime_t();
373  time_t modificationTime = fileInfo.lastModified().toTime_t();
374  time_t accessTime = fileInfo.lastRead().toTime_t();
375  return mArchive->writeDir( stripRootPath( directoryPath ), user, group, permissions, accessTime,
376  modificationTime, creationTime );
377 }
378 
379 void BackupJob::archiveNextFolder()
380 {
381  if ( mAborted )
382  return;
383 
384  if ( mPendingFolders.isEmpty() ) {
385  finish();
386  return;
387  }
388 
389  mCurrentFolder = mPendingFolders.take( 0 );
390  kdDebug(5006) << "===> Archiving next folder: " << mCurrentFolder->name() << endl;
391  mProgressItem->setStatus( i18n( "Archiving folder %1" ).arg( mCurrentFolder->name() ) );
392  if ( mCurrentFolder->open( "BackupJob" ) != 0 ) {
393  abort( i18n( "Unable to open folder '%1'.").arg( mCurrentFolder->name() ) );
394  return;
395  }
396  mCurrentFolderOpen = true;
397 
398  const TQString folderName = mCurrentFolder->name();
399  bool success = true;
400  if ( hasChildren( mCurrentFolder ) ) {
401  if ( !writeDirHelper( mCurrentFolder->subdirLocation(), mCurrentFolder->subdirLocation() ) )
402  success = false;
403  }
404  if ( !writeDirHelper( mCurrentFolder->location(), mCurrentFolder->location() ) )
405  success = false;
406  if ( !writeDirHelper( mCurrentFolder->location() + "/cur", mCurrentFolder->location() ) )
407  success = false;
408  if ( !writeDirHelper( mCurrentFolder->location() + "/new", mCurrentFolder->location() ) )
409  success = false;
410  if ( !writeDirHelper( mCurrentFolder->location() + "/tmp", mCurrentFolder->location() ) )
411  success = false;
412  if ( !success ) {
413  abort( i18n( "Unable to create folder structure for folder '%1' within archive file." )
414  .arg( mCurrentFolder->name() ) );
415  return;
416  }
417 
418  for ( int i = 0; i < mCurrentFolder->count( false /* no cache */ ); i++ ) {
419  unsigned long serNum = KMMsgDict::instance()->getMsgSerNum( mCurrentFolder, i );
420  if ( serNum == 0 ) {
421  // Uh oh
422  kdWarning(5006) << "Got serial number zero in " << mCurrentFolder->name()
423  << " at index " << i << "!" << endl;
424  // TODO: handle error in a nicer way. this is _very_ bad
425  abort( i18n( "Unable to backup messages in folder '%1', the index file is corrupted." )
426  .arg( mCurrentFolder->name() ) );
427  return;
428  }
429  else
430  mPendingMessages.append( serNum );
431  }
432  archiveNextMessage();
433 }
434 
435 // TODO
436 // - error handling
437 // - import
438 // - connect to progressmanager, especially abort
439 // - messagebox when finished (?)
440 // - ui dialog
441 // - use correct permissions
442 // - save index and serial number?
443 // - guarded pointers for folders
444 // - online IMAP: check mails first, so sernums are up-to-date?
445 // - "ignore errors"-mode, with summary how many messages couldn't be archived?
446 // - do something when the user quits KMail while the backup job is running
447 // - run in a thread?
448 // - delete source folder after completion. dangerous!!!
449 //
450 // BUGS
451 // - Online IMAP: Test Mails -> Test%20Mails
452 // - corrupted sernums indices stop backup job
453 void BackupJob::start()
454 {
455  Q_ASSERT( !mMailArchivePath.isEmpty() );
456  Q_ASSERT( mRootFolder );
457 
458  queueFolders( mRootFolder );
459 
460  switch ( mArchiveType ) {
461  case Zip: {
462  KZip *zip = new KZip( mMailArchivePath.path() );
463  zip->setCompression( KZip::DeflateCompression );
464  mArchive = zip;
465  break;
466  }
467  case Tar: {
468  mArchive = new KTar( mMailArchivePath.path(), "application/x-tar" );
469  break;
470  }
471  case TarGz: {
472  mArchive = new KTar( mMailArchivePath.path(), "application/x-gzip" );
473  break;
474  }
475  case TarBz2: {
476  mArchive = new KTar( mMailArchivePath.path(), "application/x-bzip2" );
477  break;
478  }
479  }
480 
481  kdDebug(5006) << "Starting backup." << endl;
482  if ( !mArchive->open( IO_WriteOnly ) ) {
483  abort( i18n( "Unable to open archive for writing." ) );
484  return;
485  }
486 
487  mProgressItem = KPIM::ProgressManager::createProgressItem(
488  "BackupJob",
489  i18n( "Archiving" ),
490  TQString(),
491  true );
492  mProgressItem->setUsesBusyIndicator( true );
493  connect( mProgressItem, TQ_SIGNAL(progressItemCanceled(KPIM::ProgressItem*)),
494  this, TQ_SLOT(cancelJob()) );
495 
496  archiveNextFolder();
497 }
498 
499 #include "backupjob.moc"
500 
KMail list that manages the contents of one directory that may contain folders and/or other directori...
Definition: kmfolderdir.h:16
Mail folder.
Definition: kmfolder.h:69
TQString subdirLocation() const
Returns full path to sub directory file.
Definition: kmfolder.cpp:253
KMFolderDir * child() const
Returns the folder directory associated with this node or 0 if no such directory exists.
Definition: kmfolder.h:157
KMMsgInfo * unGetMsg(int idx)
Replace KMMessage with KMMsgInfo and delete KMMessage
Definition: kmfolder.cpp:326
void close(const char *owner, bool force=false)
Close folder.
Definition: kmfolder.cpp:489
KMMessage * getMsg(int idx)
Read message at given index.
Definition: kmfolder.cpp:321
const KMMsgBase * getMsgBase(int idx) const
Provides access to the basic message fields that are also stored in the index.
Definition: kmfolder.cpp:360
int count(bool cache=false) const
Number of messages in this folder.
Definition: kmfolder.cpp:445
int open(const char *owner)
Open folder for access.
Definition: kmfolder.cpp:479
TQString location() const
Returns full path to folder file.
Definition: kmfolder.cpp:243
This is a Mime Message.
Definition: kmmessage.h:68
TQString subject() const
Get or set the 'Subject' header field.
Definition: kmmessage.cpp:2049
const DwString & asDwString() const
Return the entire message contents in the DwString.
Definition: kmmessage.cpp:292
TQString fileName() const
Get/set filename in mail folder.
Definition: kmmessage.h:802
bool isComplete() const
Return true if the complete message is available without referring to the backing store.
Definition: kmmessage.h:867
unsigned long getMsgSerNum(KMFolder *folder, int index) const
Find the message serial number for the message located at index index in folder folder.
Definition: kmmsgdict.cpp:345
void getLocation(unsigned long key, KMFolder **retFolder, int *retIndex) const
Returns the folder the message represented by the serial number key is in and the index in that folde...
Definition: kmmsgdict.cpp:319
static const KMMsgDict * instance()
Access the globally unique MessageDict.
Definition: kmmsgdict.cpp:167
folderdiaquotatab.h
Definition: aboutdata.cpp:40