1/*
2 Copyright (c) 2006 - 2007 Volker Krause <vkrause@kde.org>
3
4 This library is free software; you can redistribute it and/or modify it
5 under the terms of the GNU Library General Public License as published by
6 the Free Software Foundation; either version 2 of the License, or (at your
7 option) any later version.
8
9 This library is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12 License for more details.
13
14 You should have received a copy of the GNU Library General Public License
15 along with this library; see the file COPYING.LIB. If not, write to the
16 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 02110-1301, USA.
18*/
19
20#include "transportmanager.h"
21#include "resourcesendjob_p.h"
22#include "mailtransport_defs.h"
23#include "sendmailjob.h"
24#include "smtpjob.h"
25#include "transport.h"
26#include "transport_p.h"
27#include "transportjob.h"
28#include "transporttype.h"
29#include "transporttype_p.h"
30#include "addtransportdialog.h"
31#include "transportconfigdialog.h"
32#include "transportconfigwidget.h"
33#include "sendmailconfigwidget.h"
34#include "smtpconfigwidget.h"
35
36#include <QApplication>
37#include <QtDBus/QDBusConnection>
38#include <QtDBus/QDBusConnectionInterface>
39#include <QtDBus/QDBusServiceWatcher>
40#include <QPointer>
41#include <QRegExp>
42#include <QStringList>
43
44#include <KConfig>
45#include <KConfigGroup>
46#include <KDebug>
47#include <KEMailSettings>
48#include <KLocale>
49#include <KLocalizedString>
50#include <KMessageBox>
51#include <KRandom>
52#include <KGlobal>
53#include <KWallet/Wallet>
54
55#include <akonadi/agentinstance.h>
56#include <akonadi/agentmanager.h>
57
58using namespace MailTransport;
59using namespace KWallet;
60
61namespace MailTransport {
62/**
63 * Private class that helps to provide binary compatibility between releases.
64 * @internal
65 */
66class TransportManagerPrivate
67{
68 public:
69 TransportManagerPrivate( TransportManager *parent )
70 : q( parent )
71 {
72 }
73
74 ~TransportManagerPrivate() {
75 delete config;
76 qDeleteAll( transports );
77 }
78
79 KConfig *config;
80 QList<Transport *> transports;
81 TransportType::List types;
82 bool myOwnChange;
83 bool appliedChange;
84 KWallet::Wallet *wallet;
85 bool walletOpenFailed;
86 bool walletAsyncOpen;
87 int defaultTransportId;
88 bool isMainInstance;
89 QList<TransportJob *> walletQueue;
90 TransportManager *q;
91
92 void readConfig();
93 void writeConfig();
94 void fillTypes();
95 int createId() const;
96 void prepareWallet();
97 void validateDefault();
98 void migrateToWallet();
99
100 // Slots
101 void slotTransportsChanged();
102 void slotWalletOpened( bool success );
103 void dbusServiceUnregistered();
104 void agentTypeAdded( const Akonadi::AgentType &atype );
105 void agentTypeRemoved( const Akonadi::AgentType &atype );
106 void jobResult( KJob *job );
107};
108
109}
110
111class StaticTransportManager : public TransportManager
112{
113 public:
114 StaticTransportManager() : TransportManager() {}
115};
116
117StaticTransportManager *sSelf = 0;
118
119static void destroyStaticTransportManager() {
120 delete sSelf;
121}
122
123TransportManager::TransportManager()
124 : QObject(), d( new TransportManagerPrivate( this ) )
125{
126 KGlobal::locale()->insertCatalog( QLatin1String( "libmailtransport" ) );
127 KGlobal::locale()->insertCatalog( QLatin1String( "libakonadi-kmime" ) );
128 qAddPostRoutine( destroyStaticTransportManager );
129 d->myOwnChange = false;
130 d->appliedChange = false;
131 d->wallet = 0;
132 d->walletOpenFailed = false;
133 d->walletAsyncOpen = false;
134 d->defaultTransportId = -1;
135 d->config = new KConfig( QLatin1String( "mailtransports" ) );
136
137 QDBusConnection::sessionBus().registerObject( DBUS_OBJECT_PATH, this,
138 QDBusConnection::ExportScriptableSlots |
139 QDBusConnection::ExportScriptableSignals );
140
141 QDBusServiceWatcher *watcher =
142 new QDBusServiceWatcher( DBUS_SERVICE_NAME, QDBusConnection::sessionBus(),
143 QDBusServiceWatcher::WatchForUnregistration, this );
144 connect( watcher, SIGNAL(serviceUnregistered(QString)),
145 SLOT(dbusServiceUnregistered()) );
146
147 QDBusConnection::sessionBus().connect( QString(), QString(),
148 DBUS_INTERFACE_NAME, DBUS_CHANGE_SIGNAL,
149 this, SLOT(slotTransportsChanged()) );
150
151 d->isMainInstance = QDBusConnection::sessionBus().registerService( DBUS_SERVICE_NAME );
152
153 d->fillTypes();
154}
155
156TransportManager::~TransportManager()
157{
158 qRemovePostRoutine( destroyStaticTransportManager );
159 delete d;
160}
161
162TransportManager *TransportManager::self()
163{
164 if ( !sSelf ) {
165 sSelf = new StaticTransportManager;
166 sSelf->d->readConfig();
167 }
168 return sSelf;
169}
170
171Transport *TransportManager::transportById( int id, bool def ) const
172{
173 foreach ( Transport *t, d->transports ) {
174 if ( t->id() == id ) {
175 return t;
176 }
177 }
178
179 if ( def || ( id == 0 && d->defaultTransportId != id ) ) {
180 return transportById( d->defaultTransportId, false );
181 }
182 return 0;
183}
184
185Transport *TransportManager::transportByName( const QString &name, bool def ) const
186{
187 foreach ( Transport *t, d->transports ) {
188 if ( t->name() == name ) {
189 return t;
190 }
191 }
192 if ( def ) {
193 return transportById( 0, false );
194 }
195 return 0;
196}
197
198QList< Transport * > TransportManager::transports() const
199{
200 return d->transports;
201}
202
203TransportType::List TransportManager::types() const
204{
205 return d->types;
206}
207
208Transport *TransportManager::createTransport() const
209{
210 int id = d->createId();
211 Transport *t = new Transport( QString::number( id ) );
212 t->setId( id );
213 return t;
214}
215
216void TransportManager::addTransport( Transport *transport )
217{
218 if ( d->transports.contains( transport ) ) {
219 kDebug() << "Already have this transport.";
220 return;
221 }
222
223 kDebug() << "Added transport" << transport;
224 d->transports.append( transport );
225 d->validateDefault();
226 emitChangesCommitted();
227}
228
229void TransportManager::schedule( TransportJob *job )
230{
231 connect( job, SIGNAL(result(KJob*)), SLOT(jobResult(KJob*)) );
232
233 // check if the job is waiting for the wallet
234 if ( !job->transport()->isComplete() ) {
235 kDebug() << "job waits for wallet:" << job;
236 d->walletQueue << job;
237 loadPasswordsAsync();
238 return;
239 }
240
241 job->start();
242}
243
244void TransportManager::createDefaultTransport()
245{
246 KEMailSettings kes;
247 Transport *t = createTransport();
248 t->setName( i18n( "Default Transport" ) );
249 t->setHost( kes.getSetting( KEMailSettings::OutServer ) );
250 if ( t->isValid() ) {
251 t->writeConfig();
252 addTransport( t );
253 } else {
254 kWarning() << "KEMailSettings does not contain a valid transport.";
255 }
256}
257
258bool TransportManager::showTransportCreationDialog( QWidget *parent,
259 ShowCondition showCondition )
260{
261 if ( showCondition == IfNoTransportExists ) {
262 if ( !isEmpty() ) {
263 return true;
264 }
265
266 const int response = KMessageBox::messageBox( parent,
267 KMessageBox::WarningContinueCancel,
268 i18n( "You must create an outgoing account before sending." ),
269 i18n( "Create Account Now?" ),
270 KGuiItem( i18n( "Create Account Now" ) ) );
271 if ( response != KMessageBox::Continue ) {
272 return false;
273 }
274 }
275
276 QPointer<AddTransportDialog> dialog = new AddTransportDialog( parent );
277 const bool accepted = ( dialog->exec() == QDialog::Accepted );
278 delete dialog;
279 return accepted;
280}
281
282bool TransportManager::configureTransport( Transport *transport, QWidget *parent )
283{
284 if ( transport->type() == Transport::EnumType::Akonadi ) {
285 using namespace Akonadi;
286 AgentInstance instance = AgentManager::self()->instance( transport->host() );
287 if ( !instance.isValid() ) {
288 kWarning() << "Invalid resource instance" << transport->host();
289 }
290 instance.configure( parent ); // Async...
291 transport->writeConfig();
292 return true; // No way to know here if the user cancelled or not.
293 }
294
295 QPointer<TransportConfigDialog> transportConfigDialog =
296 new TransportConfigDialog( transport, parent );
297 transportConfigDialog->setCaption( i18n( "Configure account" ) );
298 bool okClicked = ( transportConfigDialog->exec() == QDialog::Accepted );
299 delete transportConfigDialog;
300 return okClicked;
301}
302
303TransportJob *TransportManager::createTransportJob( int transportId )
304{
305 Transport *t = transportById( transportId, false );
306 if ( !t ) {
307 return 0;
308 }
309 t = t->clone(); // Jobs delete their transports.
310 t->updatePasswordState();
311 switch ( t->type() ) {
312 case Transport::EnumType::SMTP:
313 return new SmtpJob( t, this );
314 case Transport::EnumType::Sendmail:
315 return new SendmailJob( t, this );
316 case Transport::EnumType::Akonadi:
317 return new ResourceSendJob( t, this );
318 }
319 Q_ASSERT( false );
320 return 0;
321}
322
323TransportJob *TransportManager::createTransportJob( const QString &transport )
324{
325 bool ok = false;
326 Transport *t = 0;
327
328 int transportId = transport.toInt( &ok );
329 if ( ok ) {
330 t = transportById( transportId );
331 }
332
333 if ( !t ) {
334 t = transportByName( transport, false );
335 }
336
337 if ( t ) {
338 return createTransportJob( t->id() );
339 }
340
341 return 0;
342}
343
344bool TransportManager::isEmpty() const
345{
346 return d->transports.isEmpty();
347}
348
349QList<int> TransportManager::transportIds() const
350{
351 QList<int> rv;
352 foreach ( Transport *t, d->transports ) {
353 rv << t->id();
354 }
355 return rv;
356}
357
358QStringList TransportManager::transportNames() const
359{
360 QStringList rv;
361 foreach ( Transport *t, d->transports ) {
362 rv << t->name();
363 }
364 return rv;
365}
366
367QString TransportManager::defaultTransportName() const
368{
369 Transport *t = transportById( d->defaultTransportId, false );
370 if ( t ) {
371 return t->name();
372 }
373 return QString();
374}
375
376int TransportManager::defaultTransportId() const
377{
378 return d->defaultTransportId;
379}
380
381void TransportManager::setDefaultTransport( int id )
382{
383 if ( id == d->defaultTransportId || !transportById( id, false ) ) {
384 return;
385 }
386 d->defaultTransportId = id;
387 d->writeConfig();
388}
389
390void TransportManager::removeTransport( int id )
391{
392 Transport *t = transportById( id, false );
393 if ( !t ) {
394 return;
395 }
396 emit transportRemoved( t->id(), t->name() );
397
398 // Kill the resource, if Akonadi-type transport.
399 if ( t->type() == Transport::EnumType::Akonadi ) {
400 using namespace Akonadi;
401 const AgentInstance instance = AgentManager::self()->instance( t->host() );
402 if ( !instance.isValid() ) {
403 kWarning() << "Could not find resource instance.";
404 }
405 AgentManager::self()->removeInstance( instance );
406 }
407
408 d->transports.removeAll( t );
409 d->validateDefault();
410 QString group = t->currentGroup();
411 delete t;
412 d->config->deleteGroup( group );
413 d->writeConfig();
414
415}
416
417void TransportManagerPrivate::readConfig()
418{
419 QList<Transport *> oldTransports = transports;
420 transports.clear();
421
422 QRegExp re( QLatin1String( "^Transport (.+)$" ) );
423 QStringList groups = config->groupList().filter( re );
424 foreach ( const QString &s, groups ) {
425 if (re.indexIn( s ) == -1)
426 continue;
427 Transport *t = 0;
428
429 // see if we happen to have that one already
430 foreach ( Transport *old, oldTransports ) {
431 if ( old->currentGroup() == QLatin1String( "Transport " ) + re.cap( 1 ) ) {
432 kDebug() << "reloading existing transport:" << s;
433 t = old;
434 t->d->passwordNeedsUpdateFromWallet = true;
435 t->readConfig();
436 oldTransports.removeAll( old );
437 break;
438 }
439 }
440
441 if ( !t ) {
442 t = new Transport( re.cap( 1 ) );
443 }
444 if ( t->id() <= 0 ) {
445 t->setId( createId() );
446 t->writeConfig();
447 }
448 transports.append( t );
449 }
450
451 qDeleteAll( oldTransports );
452 oldTransports.clear();
453
454 // read default transport
455 KConfigGroup group( config, "General" );
456 defaultTransportId = group.readEntry( "default-transport", 0 );
457 if ( defaultTransportId == 0 ) {
458 // migrated default transport contains the name instead
459 QString name = group.readEntry( "default-transport", QString() );
460 if ( !name.isEmpty() ) {
461 Transport *t = q->transportByName( name, false );
462 if ( t ) {
463 defaultTransportId = t->id();
464 writeConfig();
465 }
466 }
467 }
468 validateDefault();
469 migrateToWallet();
470 q->loadPasswordsAsync();
471}
472
473void TransportManagerPrivate::writeConfig()
474{
475 KConfigGroup group( config, "General" );
476 group.writeEntry( "default-transport", defaultTransportId );
477 config->sync();
478 q->emitChangesCommitted();
479}
480
481void TransportManagerPrivate::fillTypes()
482{
483 Q_ASSERT( types.isEmpty() );
484
485 // SMTP.
486 {
487 TransportType type;
488 type.d->mType = Transport::EnumType::SMTP;
489 type.d->mName = i18nc( "@option SMTP transport", "SMTP" );
490 type.d->mDescription = i18n( "An SMTP server on the Internet" );
491 types << type;
492 }
493
494 // Sendmail.
495 {
496 TransportType type;
497 type.d->mType = Transport::EnumType::Sendmail;
498 type.d->mName = i18nc( "@option sendmail transport", "Sendmail" );
499 type.d->mDescription = i18n( "A local sendmail installation" );
500 types << type;
501 }
502
503 // All Akonadi resources with MailTransport capability.
504 {
505 using namespace Akonadi;
506 foreach ( const AgentType &atype, AgentManager::self()->types() ) {
507 // TODO probably the string "MailTransport" should be #defined somewhere
508 // and used like that in the resources (?)
509 if ( atype.capabilities().contains( QLatin1String( "MailTransport" ) ) ) {
510 TransportType type;
511 type.d->mType = Transport::EnumType::Akonadi;
512 type.d->mAgentType = atype;
513 type.d->mName = atype.name();
514 type.d->mDescription = atype.description();
515 types << type;
516 kDebug() << "Found Akonadi type" << atype.name();
517 }
518 }
519
520 // Watch for appearing and disappearing types.
521 QObject::connect( AgentManager::self(), SIGNAL(typeAdded(Akonadi::AgentType)),
522 q, SLOT(agentTypeAdded(Akonadi::AgentType)) );
523 QObject::connect( AgentManager::self(), SIGNAL(typeRemoved(Akonadi::AgentType)),
524 q, SLOT(agentTypeRemoved(Akonadi::AgentType)) );
525 }
526
527 kDebug() << "Have SMTP, Sendmail, and" << types.count() - 2 << "Akonadi types.";
528}
529
530void TransportManager::emitChangesCommitted()
531{
532 d->myOwnChange = true; // prevent us from reading our changes again
533 d->appliedChange = false; // but we have to read them at least once
534 emit transportsChanged();
535 emit changesCommitted();
536}
537
538void TransportManagerPrivate::slotTransportsChanged()
539{
540 if ( myOwnChange && appliedChange ) {
541 myOwnChange = false;
542 appliedChange = false;
543 return;
544 }
545
546 kDebug();
547 config->reparseConfiguration();
548 // FIXME: this deletes existing transport objects!
549 readConfig();
550 appliedChange = true; // to prevent recursion
551 emit q->transportsChanged();
552}
553
554int TransportManagerPrivate::createId() const
555{
556 QList<int> usedIds;
557 foreach ( Transport *t, transports ) {
558 usedIds << t->id();
559 }
560 usedIds << 0; // 0 is default for unknown
561 int newId;
562 do {
563 newId = KRandom::random();
564 } while ( usedIds.contains( newId ) );
565 return newId;
566}
567
568KWallet::Wallet * TransportManager::wallet()
569{
570 if ( d->wallet && d->wallet->isOpen() ) {
571 return d->wallet;
572 }
573
574 if ( !Wallet::isEnabled() || d->walletOpenFailed ) {
575 return 0;
576 }
577
578 WId window = 0;
579 if ( qApp->activeWindow() ) {
580 window = qApp->activeWindow()->winId();
581 } else if ( !QApplication::topLevelWidgets().isEmpty() ) {
582 window = qApp->topLevelWidgets().first()->winId();
583 }
584
585 delete d->wallet;
586 d->wallet = Wallet::openWallet( Wallet::NetworkWallet(), window );
587
588 if ( !d->wallet ) {
589 d->walletOpenFailed = true;
590 return 0;
591 }
592
593 d->prepareWallet();
594 return d->wallet;
595}
596
597void TransportManagerPrivate::prepareWallet()
598{
599 if ( !wallet ) {
600 return;
601 }
602 if ( !wallet->hasFolder( WALLET_FOLDER ) ) {
603 wallet->createFolder( WALLET_FOLDER );
604 }
605 wallet->setFolder( WALLET_FOLDER );
606}
607
608void TransportManager::loadPasswords()
609{
610 foreach ( Transport *t, d->transports ) {
611 t->readPassword();
612 }
613
614 // flush the wallet queue
615 const QList<TransportJob*> copy = d->walletQueue;
616 d->walletQueue.clear();
617 foreach ( TransportJob *job, copy ) {
618 job->start();
619 }
620
621 emit passwordsChanged();
622}
623
624void TransportManager::loadPasswordsAsync()
625{
626 kDebug();
627
628 // check if there is anything to do at all
629 bool found = false;
630 foreach ( Transport *t, d->transports ) {
631 if ( !t->isComplete() ) {
632 found = true;
633 break;
634 }
635 }
636 if ( !found ) {
637 return;
638 }
639
640 // async wallet opening
641 if ( !d->wallet && !d->walletOpenFailed ) {
642 WId window = 0;
643 if ( qApp->activeWindow() ) {
644 window = qApp->activeWindow()->winId();
645 } else if ( !QApplication::topLevelWidgets().isEmpty() ) {
646 window = qApp->topLevelWidgets().first()->winId();
647 }
648
649 d->wallet = Wallet::openWallet( Wallet::NetworkWallet(), window,
650 Wallet::Asynchronous );
651 if ( d->wallet ) {
652 connect( d->wallet, SIGNAL(walletOpened(bool)), SLOT(slotWalletOpened(bool)) );
653 d->walletAsyncOpen = true;
654 } else {
655 d->walletOpenFailed = true;
656 loadPasswords();
657 }
658 return;
659 }
660 if ( d->wallet && !d->walletAsyncOpen ) {
661 loadPasswords();
662 }
663}
664
665void TransportManagerPrivate::slotWalletOpened( bool success )
666{
667 kDebug();
668 walletAsyncOpen = false;
669 if ( !success ) {
670 walletOpenFailed = true;
671 delete wallet;
672 wallet = 0;
673 } else {
674 prepareWallet();
675 }
676 q->loadPasswords();
677}
678
679void TransportManagerPrivate::validateDefault()
680{
681 if ( !q->transportById( defaultTransportId, false ) ) {
682 if ( q->isEmpty() ) {
683 defaultTransportId = -1;
684 } else {
685 defaultTransportId = transports.first()->id();
686 writeConfig();
687 }
688 }
689}
690
691void TransportManagerPrivate::migrateToWallet()
692{
693 // check if we tried this already
694 static bool firstRun = true;
695 if ( !firstRun ) {
696 return;
697 }
698 firstRun = false;
699
700 // check if we are the main instance
701 if ( !isMainInstance ) {
702 return;
703 }
704
705 // check if migration is needed
706 QStringList names;
707 foreach ( Transport *t, transports ) {
708 if ( t->needsWalletMigration() ) {
709 names << t->name();
710 }
711 }
712 if ( names.isEmpty() ) {
713 return;
714 }
715
716 // ask user if he wants to migrate
717 int result = KMessageBox::questionYesNoList(
718 0,
719 i18n( "The following mail transports store their passwords in an "
720 "unencrypted configuration file.\nFor security reasons, "
721 "please consider migrating these passwords to KWallet, the "
722 "KDE Wallet management tool,\nwhich stores sensitive data "
723 "for you in a strongly encrypted file.\n"
724 "Do you want to migrate your passwords to KWallet?" ),
725 names, i18n( "Question" ),
726 KGuiItem( i18n( "Migrate" ) ), KGuiItem( i18n( "Keep" ) ),
727 QString::fromLatin1( "WalletMigrate" ) );
728 if ( result != KMessageBox::Yes ) {
729 return;
730 }
731
732 // perform migration
733 foreach ( Transport *t, transports ) {
734 if ( t->needsWalletMigration() ) {
735 t->migrateToWallet();
736 }
737 }
738}
739
740void TransportManagerPrivate::dbusServiceUnregistered()
741{
742 QDBusConnection::sessionBus().registerService( DBUS_SERVICE_NAME );
743}
744
745void TransportManagerPrivate::agentTypeAdded( const Akonadi::AgentType &atype )
746{
747 using namespace Akonadi;
748 if ( atype.capabilities().contains( QLatin1String( "MailTransport" ) ) ) {
749 TransportType type;
750 type.d->mType = Transport::EnumType::Akonadi;
751 type.d->mAgentType = atype;
752 type.d->mName = atype.name();
753 type.d->mDescription = atype.description();
754 types << type;
755 kDebug() << "Added new Akonadi type" << atype.name();
756 }
757}
758
759void TransportManagerPrivate::agentTypeRemoved( const Akonadi::AgentType &atype )
760{
761 using namespace Akonadi;
762 foreach ( const TransportType &type, types ) {
763 if ( type.type() == Transport::EnumType::Akonadi &&
764 type.agentType() == atype ) {
765 types.removeAll( type );
766 kDebug() << "Removed Akonadi type" << atype.name();
767 }
768 }
769}
770
771void TransportManagerPrivate::jobResult( KJob *job )
772{
773 walletQueue.removeAll( static_cast<TransportJob*>( job ) );
774}
775
776#include "moc_transportmanager.cpp"
777