1 /*******************************************************************************
4 ** Created on : 03 April, 2005
5 ** Copyright : (c) 2005 Till Adam
6 ** Email : <adam@kde.org>
8 *******************************************************************************/
10 /*******************************************************************************
12 ** This program is free software; you can redistribute it and/or modify
13 ** it under the terms of the GNU General Public License as published by
14 ** the Free Software Foundation; either version 2 of the License, or
15 ** (at your option) any later version.
17 ** It is distributed in the hope that it will be useful, but
18 ** WITHOUT ANY WARRANTY; without even the implied warranty of
19 ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 ** General Public License for more details.
22 ** You should have received a copy of the GNU General Public License
23 ** along with this program; if not, write to the Free Software
24 ** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
26 ** In addition, as a special exception, the copyright holders give
27 ** permission to link the code of this program with any edition of
28 ** the Qt library by Trolltech AS, Norway (or with modified versions
29 ** of Qt that use the same license as Qt), and distribute linked
30 ** combinations including the two. You must obey the GNU General
31 ** Public License in all respects for all of the code used other than
32 ** Qt. If you modify this file, you may extend this exception to
33 ** your version of the file, but you are not obligated to do so. If
34 ** you do not wish to do so, delete this exception statement from
37 *******************************************************************************/
38 #include <config-messageviewer.h>
42 #include "iconnamecache.h"
43 #include "nodehelper.h"
44 #include "renamefiledialog.h"
46 #include "messagecore/globalsettings.h"
47 #include "messagecore/nodehelper.h"
48 #include "messagecore/stringutil.h"
50 #include <akonadi/item.h>
53 #include <kmbox/mbox.h>
55 #include <KMime/Message>
57 #include <kcharsets.h>
58 #include <KFileDialog>
61 #include <kmessagebox.h>
62 #include <kio/netaccess.h>
65 #include <KTemporaryFile>
66 #include <ktoolinvocation.h>
70 #include <QDBusInterface>
71 #include <QDBusConnectionInterface>
73 using namespace MessageViewer
;
75 bool Util::checkOverwrite( const KUrl
&url
, QWidget
*w
)
77 if ( KIO::NetAccess::exists( url
, KIO::NetAccess::DestinationSide
, w
) ) {
78 if ( KMessageBox::Cancel
== KMessageBox::warningContinueCancel(
80 i18n( "A file named \"%1\" already exists. "
81 "Are you sure you want to overwrite it?", url
.prettyUrl() ),
82 i18n( "Overwrite File?" ),
83 KStandardGuiItem::overwrite() ) )
89 QString
Util::fileNameForMimetype( const QString
&mimeType
, int iconSize
,
90 const QString
&fallbackFileName1
,
91 const QString
&fallbackFileName2
)
94 QString tMimeType
= mimeType
;
96 // convert non-registered types to registered types
97 if ( mimeType
== QLatin1String( "application/x-vnd.kolab.contact" ) ) {
98 tMimeType
= QLatin1String( "text/x-vcard" );
99 } else if ( mimeType
== QLatin1String( "application/x-vnd.kolab.event" ) ) {
100 tMimeType
= QLatin1String( "application/x-vnd.akonadi.calendar.event" );
101 } else if ( mimeType
== QLatin1String( "application/x-vnd.kolab.task" ) ) {
102 tMimeType
= QLatin1String( "application/x-vnd.akonadi.calendar.todo" );
103 } else if ( mimeType
== QLatin1String( "application/x-vnd.kolab.journal" ) ) {
104 tMimeType
= QLatin1String( "application/x-vnd.akonadi.calendar.journal" );
105 } else if ( mimeType
== QLatin1String( "application/x-vnd.kolab.note" ) ) {
106 tMimeType
= QLatin1String( "application/x-vnd.akonadi.note" );
109 KMimeType::Ptr mime
= KMimeType::mimeType( tMimeType
, KMimeType::ResolveAliases
);
111 fileName
= mime
->iconName();
113 fileName
= QLatin1String( "unknown" );
114 if ( !tMimeType
.isEmpty() ) {
115 kWarning() << "unknown mimetype" << tMimeType
;
119 if ( fileName
.isEmpty() ) {
120 fileName
= fallbackFileName1
;
121 if ( fileName
.isEmpty() ) {
122 fileName
= fallbackFileName2
;
124 if ( !fileName
.isEmpty() ) {
125 fileName
= KMimeType::findByPath( "/tmp/" + fileName
, 0, true )->iconName();
129 return IconNameCache::instance()->iconPath( fileName
, iconSize
);
132 #if defined Q_WS_WIN || defined Q_WS_MACX
133 #include <QDesktopServices>
136 bool Util::handleUrlWithQDesktopServices( const KUrl
& url
)
138 #if defined Q_WS_WIN || defined Q_WS_MACX
139 QDesktopServices::openUrl( url
);
147 QList
<KMime::Content
*> Util::allContents( const KMime::Content
*message
)
149 KMime::Content::List result
;
150 KMime::Content
*child
= MessageCore::NodeHelper::firstChild( message
);
153 result
+= allContents( child
);
155 KMime::Content
*next
= MessageCore::NodeHelper::nextSibling( message
);
158 result
+= allContents( next
);
164 QList
<KMime::Content
*> Util::extractAttachments( const KMime::Message
*message
)
166 const KMime::Content::List contents
= allContents( message
);
167 KMime::Content::List result
;
168 for ( KMime::Content::List::const_iterator it
= contents
.constBegin();
169 it
!= contents
.constEnd(); ) {
170 KMime::Content
* content
= *it
;
171 if ( content
->contentDisposition()->filename().trimmed().isEmpty() &&
172 ( content
->contentType()->name().trimmed().isEmpty() ||
173 content
== message
) ) {
184 bool Util::saveContents( QWidget
*parent
, const QList
<KMime::Content
*> &contents
)
187 if ( contents
.count() > 1 ) {
189 dirUrl
= KFileDialog::getExistingDirectoryUrl( KUrl( "kfiledialog:///saveAttachment" ),
191 i18n( "Save Attachments To" ) );
192 if ( !dirUrl
.isValid() ) {
196 // we may not get a slash-terminated url out of KFileDialog
197 dirUrl
.adjustPath( KUrl::AddTrailingSlash
);
200 // only one item, get the desired filename
201 KMime::Content
*content
= contents
.first();
202 QString fileName
= NodeHelper::fileName( content
);
203 fileName
= MessageCore::StringUtil::cleanFileName( fileName
);
204 if ( fileName
.isEmpty() ) {
205 fileName
= i18nc( "filename for an unnamed attachment", "attachment.1" );
207 url
= KFileDialog::getSaveUrl( KUrl( "kfiledialog:///saveAttachment/" + fileName
),
210 i18n( "Save Attachment" ) );
211 if ( url
.isEmpty() ) {
216 QMap
< QString
, int > renameNumbering
;
218 bool globalResult
= true;
219 int unnamedAtmCount
= 0;
220 MessageViewer::RenameFileDialog::RenameFileDialogResult result
= MessageViewer::RenameFileDialog::RENAMEFILE_IGNORE
;
221 foreach( KMime::Content
*content
, contents
) {
223 if ( !dirUrl
.isEmpty() ) {
225 QString fileName
= MessageViewer::NodeHelper::fileName( content
);
226 fileName
= MessageCore::StringUtil::cleanFileName( fileName
);
227 if ( fileName
.isEmpty() ) {
229 fileName
= i18nc( "filename for the %1-th unnamed attachment",
230 "attachment.%1", unnamedAtmCount
);
232 curUrl
.setFileName( fileName
);
237 if ( !curUrl
.isEmpty() ) {
239 // Rename the file if we have already saved one with the same name:
240 // try appending a number before extension (e.g. "pic.jpg" => "pic_2.jpg")
241 QString origFile
= curUrl
.fileName();
242 QString file
= origFile
;
244 while ( renameNumbering
.contains(file
) ) {
246 int num
= renameNumbering
[file
] + 1;
247 int dotIdx
= file
.lastIndexOf('.');
248 file
= file
.insert( (dotIdx
>=0) ? dotIdx
: file
.length(), QString("_") + QString::number(num
) );
250 curUrl
.setFileName(file
);
252 // Increment the counter for both the old and the new filename
253 if ( !renameNumbering
.contains(origFile
))
254 renameNumbering
[origFile
] = 1;
256 renameNumbering
[origFile
]++;
258 if ( file
!= origFile
) {
259 if ( !renameNumbering
.contains(file
))
260 renameNumbering
[file
] = 1;
262 renameNumbering
[file
]++;
266 if( !(result
== MessageViewer::RenameFileDialog::RENAMEFILE_OVERWRITEALL
||
267 result
== MessageViewer::RenameFileDialog::RENAMEFILE_IGNOREALL
))
269 if ( KIO::NetAccess::exists( curUrl
, KIO::NetAccess::DestinationSide
, parent
) ) {
270 if ( contents
.count() == 1 ) {
271 RenameFileDialog
*dlg
= new RenameFileDialog(curUrl
,false, parent
);
272 result
= static_cast<MessageViewer::RenameFileDialog::RenameFileDialogResult
>(dlg
->exec());
273 if ( result
== MessageViewer::RenameFileDialog::RENAMEFILE_IGNORE
)
278 else if ( result
== MessageViewer::RenameFileDialog::RENAMEFILE_RENAME
)
280 curUrl
= dlg
->newName();
285 RenameFileDialog
*dlg
= new RenameFileDialog(curUrl
,true, parent
);
286 result
= static_cast<MessageViewer::RenameFileDialog::RenameFileDialogResult
>(dlg
->exec());
288 if ( result
== MessageViewer::RenameFileDialog::RENAMEFILE_IGNORE
||
289 result
== MessageViewer::RenameFileDialog::RENAMEFILE_IGNOREALL
)
294 else if ( result
== MessageViewer::RenameFileDialog::RENAMEFILE_RENAME
)
296 curUrl
= dlg
->newName();
303 if( result
!= MessageViewer::RenameFileDialog::RENAMEFILE_IGNOREALL
) {
304 const bool result
= saveContent( parent
, content
, curUrl
);
306 globalResult
= result
;
314 bool Util::saveContent( QWidget
*parent
, KMime::Content
* content
, const KUrl
& url
)
316 // FIXME: This is all horribly broken. First of all, creating a NodeHelper and then immediatley
317 // reading out the encryption/signature state will not work at all.
318 // Then, topLevel() will not work for attachments that are inside encrypted parts.
319 // What should actually be done is either passing in an ObjectTreeParser that has already
320 // parsed the message, or creating an OTP here (which would have the downside that the
321 // password dialog for decrypting messages is shown twice)
322 #if 0 // totally broken
323 KMime::Content
*topContent
= content
->topLevel();
324 MessageViewer::NodeHelper
*mNodeHelper
= new MessageViewer::NodeHelper
;
325 bool bSaveEncrypted
= false;
326 bool bEncryptedParts
= mNodeHelper
->encryptionState( content
) != MessageViewer::KMMsgNotEncrypted
;
327 if( bEncryptedParts
)
328 if( KMessageBox::questionYesNo( parent
,
329 i18n( "The part %1 of the message is encrypted. Do you want to keep the encryption when saving?",
331 i18n( "KMail Question" ), KGuiItem(i18n("Keep Encryption")), KGuiItem(i18n("Do Not Keep")) ) ==
333 bSaveEncrypted
= true;
335 bool bSaveWithSig
= true;
336 if(mNodeHelper
->signatureState( content
) != MessageViewer::KMMsgNotSigned
)
337 if( KMessageBox::questionYesNo( parent
,
338 i18n( "The part %1 of the message is signed. Do you want to keep the signature when saving?",
340 i18n( "KMail Question" ), KGuiItem(i18n("Keep Signature")), KGuiItem(i18n("Do Not Keep")) ) !=
342 bSaveWithSig
= false;
345 if( bSaveEncrypted
|| !bEncryptedParts
) {
346 KMime::Content
*dataNode
= content
;
347 QByteArray rawDecryptedBody
;
348 bool gotRawDecryptedBody
= false;
349 if ( !bSaveWithSig
) {
350 if ( topContent
->contentType()->mimeType() == "multipart/signed" ) {
351 // carefully look for the part that is *not* the signature part:
352 if ( ObjectTreeParser::findType( topContent
, "application/pgp-signature", true, false ) ) {
353 dataNode
= ObjectTreeParser::findTypeNot( topContent
, "application", "pgp-signature", true, false );
354 } else if ( ObjectTreeParser::findType( topContent
, "application/pkcs7-mime" , true, false ) ) {
355 dataNode
= ObjectTreeParser::findTypeNot( topContent
, "application", "pkcs7-mime", true, false );
357 dataNode
= ObjectTreeParser::findTypeNot( topContent
, "multipart", "", true, false );
360 EmptySource emptySource
;
361 ObjectTreeParser
otp( &emptySource
, 0, 0, false, false );
363 // process this node and all it's siblings and descendants
364 mNodeHelper
->setNodeUnprocessed( dataNode
, true );
365 otp
.parseObjectTree( dataNode
);
367 rawDecryptedBody
= otp
.rawDecryptedBody();
368 gotRawDecryptedBody
= true;
371 QByteArray cstr
= gotRawDecryptedBody
373 : dataNode
->decodedContent();
374 data
= KMime::CRLFtoLF( cstr
);
377 const QByteArray data
= content
->decodedContent();
378 kWarning() << "Port the encryption/signature handling when saving a KMime::Content.";
383 if ( url
.isLocalFile() )
386 file
.setFileName( url
.toLocalFile() );
387 if ( !file
.open( QIODevice::WriteOnly
) )
389 KMessageBox::error( parent
,
390 i18nc( "1 = file name, 2 = error string",
391 "<qt>Could not write to the file<br><filename>%1</filename><br><br>%2",
393 file
.errorString() ),
394 i18n( "Error saving attachment" ) );
398 const int permissions
= MessageViewer::Util::getWritePermissions();
399 if ( permissions
>= 0 )
400 fchmod( file
.handle(), permissions
);
402 ds
.setDevice( &file
);
405 // tmp file for upload
410 const int bytesWritten
= ds
.writeRawData( data
.data(), data
.size() );
411 if ( bytesWritten
!= data
.size() ) {
412 QFile
*f
= static_cast<QFile
*>( ds
.device() );
413 KMessageBox::error( parent
,
414 i18nc( "1 = file name, 2 = error string",
415 "<qt>Could not write to the file<br><filename>%1</filename><br><br>%2",
418 i18n( "Error saving attachment" ) );
419 // Remove the newly created empty or partial file
424 if ( !url
.isLocalFile() )
426 // QTemporaryFile::fileName() is only defined while the file is open
427 QString tfName
= tf
.fileName();
429 if ( !KIO::NetAccess::upload( tfName
, url
, parent
) )
431 KMessageBox::error( parent
,
432 i18nc( "1 = file name, 2 = error string",
433 "<qt>Could not write to the file<br><filename>%1</filename><br><br>%2",
435 KIO::NetAccess::lastErrorString() ),
436 i18n( "Error saving attachment" ) );
444 mNodeHelper
->removeTempFiles();
451 int Util::getWritePermissions()
453 // #79685, #232001 by default use the umask the user defined, but let it be configurable
454 if ( MessageCore::GlobalSettings::self()->disregardUmask() ) {
455 return S_IRUSR
| S_IWUSR
;
461 bool Util::saveAttachments( const KMime::Content::List
& contents
, QWidget
*parent
)
463 if ( contents
.isEmpty() ) {
464 KMessageBox::information( parent
, i18n( "Found no attachments to save." ) );
468 return Util::saveContents( parent
, contents
);
471 bool Util::saveMessageInMbox( const QList
<Akonadi::Item
>& retrievedMsgs
, QWidget
*parent
, bool appendMessages
)
475 if ( retrievedMsgs
.isEmpty() )
477 const Akonadi::Item msgBase
= retrievedMsgs
.first();
479 if( msgBase
.hasPayload
<KMime::Message::Ptr
>() )
480 fileName
= MessageCore::StringUtil::cleanFileName(MessageViewer::NodeHelper::cleanSubject ( msgBase
.payload
<KMime::Message::Ptr
>().get() ).trimmed() );
482 fileName
= i18n("message");
484 if ( !fileName
.endsWith( QLatin1String( ".mbox" ) ) )
487 const QString filter
= i18n( "*.mbox|email messages (*.mbox)\n*|all files (*)" );
488 KFileDialog::Option options
= static_cast<KFileDialog::Option
>(0);
489 if( !appendMessages
)
490 options
= KFileDialog::ConfirmOverwrite
;
491 const KUrl url
= KFileDialog::getSaveUrl( KUrl::fromPath( fileName
), filter
, parent
, i18np("Save Message", "Save Messages", retrievedMsgs
.count() ), options
);
496 const QString localFileName
= url
.toLocalFile();
497 if ( localFileName
.isEmpty() )
500 if( !appendMessages
) {
501 QFile::remove(localFileName
);
505 if ( !mbox
.load( localFileName
) ) {
506 if( appendMessages
) {
507 KMessageBox::error( parent
, i18n("File %1 could not be loaded.",localFileName
) , i18n( "Error loading message" ) );
509 KMessageBox::error( parent
, i18n("File %1 could not be created.",localFileName
) , i18n( "Error saving message" ) );
513 foreach ( const Akonadi::Item
&item
, retrievedMsgs
) {
514 if ( item
.hasPayload
<KMime::Message::Ptr
>() ) {
515 mbox
.appendMessage( item
.payload
<KMime::Message::Ptr
>() );
519 if ( !mbox
.save() ) {
520 KMessageBox::error( parent
, i18n("We can not save message.") , i18n( "Error saving message" ) );
527 bool Util::speakSelectedText( const QString
& text
, QWidget
*parent
)
532 // If KTTSD not running, start it.
533 if (!QDBusConnection::sessionBus().interface()->isServiceRegistered("org.kde.kttsd"))
536 if (KToolInvocation::startServiceByDesktopName("kttsd", QStringList(), &error
))
538 KMessageBox::error(parent
, i18n( "Starting Jovie Text-to-Speech Service Failed"), error
);
542 QDBusInterface
ktts("org.kde.kttsd", "/KSpeech", "org.kde.KSpeech");
543 ktts
.asyncCall("say", text
, 0);