1 /*
2   SPDX-FileCopyrightText: 2006-2007 Volker Krause <vkrause@kde.org>
3 
4   SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "transportmanager.h"
8 #include "mailtransport_defs.h"
9 #include "plugins/transportabstractplugin.h"
10 #include "plugins/transportpluginmanager.h"
11 #include "transport.h"
12 #include "transport_p.h"
13 #include "transportjob.h"
14 #include "transporttype.h"
15 #include "transporttype_p.h"
16 #include "widgets/addtransportdialogng.h"
17 #include <MailTransport/TransportAbstractPlugin>
18 
19 #include <QApplication>
20 #include <QDBusConnection>
21 #include <QDBusConnectionInterface>
22 #include <QDBusServiceWatcher>
23 #include <QPointer>
24 #include <QRandomGenerator>
25 #include <QRegularExpression>
26 #include <QStringList>
27 
28 #include "mailtransport_debug.h"
29 #include <KConfig>
30 #include <KConfigGroup>
31 #include <KEMailSettings>
32 #include <KLocalizedString>
33 #include <KMessageBox>
34 #include <kcoreaddons_version.h>
35 #if KCOREADDONS_VERSION < QT_VERSION_CHECK(6, 0, 0)
36 #include <Kdelibs4ConfigMigrator>
37 #endif
38 #include <qt5keychain/keychain.h>
39 using namespace QKeychain;
40 #include <KWallet>
41 
42 using namespace MailTransport;
43 using namespace KWallet;
44 
45 namespace MailTransport
46 {
47 /**
48  * Private class that helps to provide binary compatibility between releases.
49  * @internal
50  */
51 class TransportManagerPrivate
52 {
53 public:
TransportManagerPrivate(TransportManager * parent)54     TransportManagerPrivate(TransportManager *parent)
55         : q(parent)
56     {
57     }
58 
~TransportManagerPrivate()59     ~TransportManagerPrivate()
60     {
61         delete config;
62         qDeleteAll(transports);
63     }
64 
65     KConfig *config = nullptr;
66     QList<Transport *> transports;
67     TransportType::List types;
68     bool myOwnChange = false;
69     bool appliedChange = false;
70     KWallet::Wallet *wallet = nullptr;
71     bool walletOpenFailed = false;
72     bool walletAsyncOpen = false;
73     int defaultTransportId = -1;
74     bool isMainInstance = false;
75     QList<TransportJob *> walletQueue;
76     TransportManager *const q;
77 
78     void readConfig();
79     void writeConfig();
80     void fillTypes();
81     int createId() const;
82     void prepareWallet();
83     void validateDefault();
84     void migrateToWallet();
85     void updatePluginList();
86 
87     // Slots
88     void slotTransportsChanged();
89     void slotWalletOpened(bool success);
90     void dbusServiceUnregistered();
91     void jobResult(KJob *job);
92 };
93 }
94 
95 class StaticTransportManager : public TransportManager
96 {
97 public:
StaticTransportManager()98     StaticTransportManager()
99         : TransportManager()
100     {
101     }
102 };
103 
104 StaticTransportManager *sSelf = nullptr;
105 
destroyStaticTransportManager()106 static void destroyStaticTransportManager()
107 {
108     delete sSelf;
109 }
110 
TransportManager()111 TransportManager::TransportManager()
112     : QObject()
113     , d(new TransportManagerPrivate(this))
114 {
115 #if KCOREADDONS_VERSION < QT_VERSION_CHECK(6, 0, 0)
116     Kdelibs4ConfigMigrator migrate(QStringLiteral("transportmanager"));
117     migrate.setConfigFiles(QStringList() << QStringLiteral("mailtransports"));
118     migrate.migrate();
119 #endif
120     qAddPostRoutine(destroyStaticTransportManager);
121     d->config = new KConfig(QStringLiteral("mailtransports"));
122 
123     QDBusConnection::sessionBus().registerObject(DBUS_OBJECT_PATH, this, QDBusConnection::ExportScriptableSlots | QDBusConnection::ExportScriptableSignals);
124 
125     auto watcher = new QDBusServiceWatcher(DBUS_SERVICE_NAME, QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForUnregistration, this);
126     connect(watcher, &QDBusServiceWatcher::serviceUnregistered, this, [this]() {
127         d->dbusServiceUnregistered();
128     });
129 
130     QDBusConnection::sessionBus().connect(QString(), QString(), DBUS_INTERFACE_NAME, DBUS_CHANGE_SIGNAL, this, SLOT(slotTransportsChanged()));
131 
132     d->isMainInstance = QDBusConnection::sessionBus().registerService(DBUS_SERVICE_NAME);
133 
134     d->fillTypes();
135 }
136 
~TransportManager()137 TransportManager::~TransportManager()
138 {
139     qRemovePostRoutine(destroyStaticTransportManager);
140 }
141 
self()142 TransportManager *TransportManager::self()
143 {
144     if (!sSelf) {
145         sSelf = new StaticTransportManager;
146         sSelf->d->readConfig();
147     }
148     return sSelf;
149 }
150 
transportById(int id,bool def) const151 Transport *TransportManager::transportById(int id, bool def) const
152 {
153     for (Transport *t : std::as_const(d->transports)) {
154         if (t->id() == id) {
155             return t;
156         }
157     }
158 
159     if (def || (id == 0 && d->defaultTransportId != id)) {
160         return transportById(d->defaultTransportId, false);
161     }
162     return nullptr;
163 }
164 
transportByName(const QString & name,bool def) const165 Transport *TransportManager::transportByName(const QString &name, bool def) const
166 {
167     for (Transport *t : std::as_const(d->transports)) {
168         if (t->name() == name) {
169             return t;
170         }
171     }
172     if (def) {
173         return transportById(0, false);
174     }
175     return nullptr;
176 }
177 
transports() const178 QList<Transport *> TransportManager::transports() const
179 {
180     return d->transports;
181 }
182 
types() const183 TransportType::List TransportManager::types() const
184 {
185     return d->types;
186 }
187 
createTransport() const188 Transport *TransportManager::createTransport() const
189 {
190     int id = d->createId();
191     auto t = new Transport(QString::number(id));
192     t->setId(id);
193     return t;
194 }
195 
addTransport(Transport * transport)196 void TransportManager::addTransport(Transport *transport)
197 {
198     if (d->transports.contains(transport)) {
199         qCDebug(MAILTRANSPORT_LOG) << "Already have this transport.";
200         return;
201     }
202 
203     qCDebug(MAILTRANSPORT_LOG) << "Added transport" << transport;
204     d->transports.append(transport);
205     d->validateDefault();
206     emitChangesCommitted();
207 }
208 
schedule(TransportJob * job)209 void TransportManager::schedule(TransportJob *job)
210 {
211     connect(job, &TransportJob::result, this, [this](KJob *job) {
212         d->jobResult(job);
213     });
214 
215     // check if the job is waiting for the wallet
216     if (!job->transport()->isComplete()) {
217         qCDebug(MAILTRANSPORT_LOG) << "job waits for wallet:" << job;
218         d->walletQueue << job;
219         loadPasswordsAsync();
220         return;
221     }
222 
223     job->start();
224 }
225 
createDefaultTransport()226 void TransportManager::createDefaultTransport()
227 {
228     KEMailSettings kes;
229     Transport *t = createTransport();
230     t->setName(i18n("Default Transport"));
231     t->setHost(kes.getSetting(KEMailSettings::OutServer));
232     if (t->isValid()) {
233         t->save();
234         addTransport(t);
235     } else {
236         qCWarning(MAILTRANSPORT_LOG) << "KEMailSettings does not contain a valid transport.";
237     }
238 }
239 
showTransportCreationDialog(QWidget * parent,ShowCondition showCondition)240 bool TransportManager::showTransportCreationDialog(QWidget *parent, ShowCondition showCondition)
241 {
242     if (showCondition == IfNoTransportExists) {
243         if (!isEmpty()) {
244             return true;
245         }
246 
247         const int response = KMessageBox::messageBox(parent,
248                                                      KMessageBox::WarningContinueCancel,
249                                                      i18n("You must create an outgoing account before sending."),
250                                                      i18n("Create Account Now?"),
251                                                      KGuiItem(i18n("Create Account Now")));
252         if (response != KMessageBox::Continue) {
253             return false;
254         }
255     }
256 
257     QPointer<AddTransportDialogNG> dialog = new AddTransportDialogNG(parent);
258     const bool accepted = (dialog->exec() == QDialog::Accepted);
259     delete dialog;
260     return accepted;
261 }
262 
initializeTransport(const QString & identifier,Transport * transport)263 void TransportManager::initializeTransport(const QString &identifier, Transport *transport)
264 {
265     TransportAbstractPlugin *plugin = TransportPluginManager::self()->plugin(identifier);
266     if (plugin) {
267         plugin->initializeTransport(transport, identifier);
268     }
269 }
270 
configureTransport(const QString & identifier,Transport * transport,QWidget * parent)271 bool TransportManager::configureTransport(const QString &identifier, Transport *transport, QWidget *parent)
272 {
273     TransportAbstractPlugin *plugin = TransportPluginManager::self()->plugin(identifier);
274     if (plugin) {
275         return plugin->configureTransport(identifier, transport, parent);
276     }
277     return false;
278 }
279 
createTransportJob(int transportId)280 TransportJob *TransportManager::createTransportJob(int transportId)
281 {
282     Transport *t = transportById(transportId, false);
283     if (!t) {
284         return nullptr;
285     }
286     t = t->clone(); // Jobs delete their transports.
287     t->updatePasswordState();
288     TransportAbstractPlugin *plugin = TransportPluginManager::self()->plugin(t->identifier());
289     if (plugin) {
290         return plugin->createTransportJob(t, t->identifier());
291     }
292     Q_ASSERT(false);
293     return nullptr;
294 }
295 
createTransportJob(const QString & transport)296 TransportJob *TransportManager::createTransportJob(const QString &transport)
297 {
298     bool ok = false;
299     Transport *t = nullptr;
300 
301     int transportId = transport.toInt(&ok);
302     if (ok) {
303         t = transportById(transportId);
304     }
305 
306     if (!t) {
307         t = transportByName(transport, false);
308     }
309 
310     if (t) {
311         return createTransportJob(t->id());
312     }
313 
314     return nullptr;
315 }
316 
isEmpty() const317 bool TransportManager::isEmpty() const
318 {
319     return d->transports.isEmpty();
320 }
321 
transportIds() const322 QVector<int> TransportManager::transportIds() const
323 {
324     QVector<int> rv;
325     rv.reserve(d->transports.count());
326     for (Transport *t : std::as_const(d->transports)) {
327         rv << t->id();
328     }
329     return rv;
330 }
331 
transportNames() const332 QStringList TransportManager::transportNames() const
333 {
334     QStringList rv;
335     rv.reserve(d->transports.count());
336     for (Transport *t : std::as_const(d->transports)) {
337         rv << t->name();
338     }
339     return rv;
340 }
341 
defaultTransportName() const342 QString TransportManager::defaultTransportName() const
343 {
344     Transport *t = transportById(d->defaultTransportId, false);
345     if (t) {
346         return t->name();
347     }
348     return QString();
349 }
350 
defaultTransportId() const351 int TransportManager::defaultTransportId() const
352 {
353     return d->defaultTransportId;
354 }
355 
setDefaultTransport(int id)356 void TransportManager::setDefaultTransport(int id)
357 {
358     if (id == d->defaultTransportId || !transportById(id, false)) {
359         return;
360     }
361     d->defaultTransportId = id;
362     d->writeConfig();
363 }
364 
removePasswordFromWallet(int id)365 void TransportManager::removePasswordFromWallet(int id)
366 {
367     auto deleteJob = new DeletePasswordJob(WALLET_FOLDER);
368     deleteJob->setKey(QString::number(id));
369     deleteJob->start();
370 }
371 
removeTransport(int id)372 void TransportManager::removeTransport(int id)
373 {
374     Transport *t = transportById(id, false);
375     if (!t) {
376         return;
377     }
378     auto plugin = MailTransport::TransportPluginManager::self()->plugin(t->identifier());
379     if (plugin) {
380         plugin->cleanUp(t);
381     }
382     Q_EMIT transportRemoved(t->id(), t->name());
383 
384     d->transports.removeAll(t);
385     d->validateDefault();
386     const QString group = t->currentGroup();
387     if (t->storePassword()) {
388         auto deleteJob = new DeletePasswordJob(WALLET_FOLDER);
389         deleteJob->setKey(QString::number(t->id()));
390         deleteJob->start();
391     }
392     delete t;
393     d->config->deleteGroup(group);
394     d->writeConfig();
395 }
396 
readConfig()397 void TransportManagerPrivate::readConfig()
398 {
399     QList<Transport *> oldTransports = transports;
400     transports.clear();
401 
402     static QRegularExpression re(QStringLiteral("^Transport (.+)$"));
403     const QStringList groups = config->groupList().filter(re);
404     for (const QString &s : groups) {
405         const QRegularExpressionMatch match = re.match(s);
406         if (!match.hasMatch()) {
407             continue;
408         }
409         Transport *t = nullptr;
410         // see if we happen to have that one already
411         const QString capturedString = match.captured(1);
412         const QString checkString = QLatin1String("Transport ") + capturedString;
413         for (Transport *old : oldTransports) {
414             if (old->currentGroup() == checkString) {
415                 qCDebug(MAILTRANSPORT_LOG) << "reloading existing transport:" << s;
416                 t = old;
417                 t->load();
418                 oldTransports.removeAll(old);
419                 break;
420             }
421         }
422 
423         if (!t) {
424             t = new Transport(capturedString);
425         }
426         if (t->id() <= 0) {
427             t->setId(createId());
428             t->save();
429         }
430         transports.append(t);
431     }
432 
433     qDeleteAll(oldTransports);
434     oldTransports.clear();
435     // read default transport
436     KConfigGroup group(config, "General");
437     defaultTransportId = group.readEntry("default-transport", 0);
438     if (defaultTransportId == 0) {
439         // migrated default transport contains the name instead
440         QString name = group.readEntry("default-transport", QString());
441         if (!name.isEmpty()) {
442             Transport *t = q->transportByName(name, false);
443             if (t) {
444                 defaultTransportId = t->id();
445                 writeConfig();
446             }
447         }
448     }
449     validateDefault();
450     migrateToWallet();
451     q->loadPasswordsAsync();
452 }
453 
writeConfig()454 void TransportManagerPrivate::writeConfig()
455 {
456     KConfigGroup group(config, "General");
457     group.writeEntry("default-transport", defaultTransportId);
458     config->sync();
459     q->emitChangesCommitted();
460 }
461 
fillTypes()462 void TransportManagerPrivate::fillTypes()
463 {
464     Q_ASSERT(types.isEmpty());
465 
466     updatePluginList();
467     QObject::connect(MailTransport::TransportPluginManager::self(), &TransportPluginManager::updatePluginList, q, &TransportManager::updatePluginList);
468 }
469 
updatePluginList()470 void TransportManagerPrivate::updatePluginList()
471 {
472     types.clear();
473     const QVector<MailTransport::TransportAbstractPlugin *> lstPlugins = MailTransport::TransportPluginManager::self()->pluginsList();
474     for (MailTransport::TransportAbstractPlugin *plugin : lstPlugins) {
475         if (plugin->names().isEmpty()) {
476             qCDebug(MAILTRANSPORT_LOG) << "Plugin " << plugin << " doesn't provide plugin";
477         }
478         const QVector<TransportAbstractPluginInfo> lstInfos = plugin->names();
479         for (const MailTransport::TransportAbstractPluginInfo &info : lstInfos) {
480             TransportType type;
481             type.d->mName = info.name;
482             type.d->mDescription = info.description;
483             type.d->mIdentifier = info.identifier;
484             type.d->mIsAkonadiResource = info.isAkonadi;
485             types << type;
486         }
487     }
488 }
489 
updatePluginList()490 void TransportManager::updatePluginList()
491 {
492     d->updatePluginList();
493 }
494 
emitChangesCommitted()495 void TransportManager::emitChangesCommitted()
496 {
497     d->myOwnChange = true; // prevent us from reading our changes again
498     d->appliedChange = false; // but we have to read them at least once
499     Q_EMIT transportsChanged();
500     Q_EMIT changesCommitted();
501 }
502 
slotTransportsChanged()503 void TransportManagerPrivate::slotTransportsChanged()
504 {
505     if (myOwnChange && appliedChange) {
506         myOwnChange = false;
507         appliedChange = false;
508         return;
509     }
510 
511     qCDebug(MAILTRANSPORT_LOG);
512     config->reparseConfiguration();
513     // FIXME: this deletes existing transport objects!
514     readConfig();
515     appliedChange = true; // to prevent recursion
516     Q_EMIT q->transportsChanged();
517 }
518 
createId() const519 int TransportManagerPrivate::createId() const
520 {
521     QVector<int> usedIds;
522     usedIds.reserve(transports.size());
523     for (Transport *t : std::as_const(transports)) {
524         usedIds << t->id();
525     }
526     int newId;
527     do {
528         // 0 is default for unknown, so use 1 as lower value accepted
529         newId = QRandomGenerator::global()->bounded(1, RAND_MAX);
530     } while (usedIds.contains(newId));
531     return newId;
532 }
533 
wallet()534 KWallet::Wallet *TransportManager::wallet()
535 {
536     if (d->wallet && d->wallet->isOpen()) {
537         return d->wallet;
538     }
539 
540     if (!Wallet::isEnabled() || d->walletOpenFailed) {
541         return nullptr;
542     }
543 
544     WId window = 0;
545     if (qApp->activeWindow()) {
546         window = qApp->activeWindow()->winId();
547     } else if (!QApplication::topLevelWidgets().isEmpty()) {
548         window = qApp->topLevelWidgets().first()->winId();
549     }
550 
551     delete d->wallet;
552     d->wallet = Wallet::openWallet(Wallet::NetworkWallet(), window);
553 
554     if (!d->wallet) {
555         d->walletOpenFailed = true;
556         return nullptr;
557     }
558 
559     d->prepareWallet();
560     return d->wallet;
561 }
562 
prepareWallet()563 void TransportManagerPrivate::prepareWallet()
564 {
565     if (!wallet) {
566         return;
567     }
568     if (!wallet->hasFolder(WALLET_FOLDER)) {
569         wallet->createFolder(WALLET_FOLDER);
570     }
571     wallet->setFolder(WALLET_FOLDER);
572 }
573 
loadPasswords()574 void TransportManager::loadPasswords()
575 {
576     for (Transport *t : std::as_const(d->transports)) {
577         t->readPassword();
578     }
579 
580     // flush the wallet queue
581     const QList<TransportJob *> copy = d->walletQueue;
582     d->walletQueue.clear();
583     for (TransportJob *job : copy) {
584         job->start();
585     }
586 
587     Q_EMIT passwordsChanged();
588 }
589 
loadPasswordsAsync()590 void TransportManager::loadPasswordsAsync()
591 {
592     qCDebug(MAILTRANSPORT_LOG);
593 
594     // check if there is anything to do at all
595     bool found = false;
596     for (Transport *t : std::as_const(d->transports)) {
597         if (!t->isComplete()) {
598             found = true;
599             break;
600         }
601     }
602     if (!found) {
603         return;
604     }
605 
606     // async wallet opening
607     if (!d->wallet && !d->walletOpenFailed) {
608         WId window = 0;
609         if (qApp->activeWindow()) {
610             window = qApp->activeWindow()->winId();
611         } else if (!QApplication::topLevelWidgets().isEmpty()) {
612             window = qApp->topLevelWidgets().first()->winId();
613         }
614 
615         d->wallet = Wallet::openWallet(Wallet::NetworkWallet(), window, Wallet::Asynchronous);
616         // Already async. It will be easy to port to qt5keychain
617         if (d->wallet) {
618             connect(d->wallet, &KWallet::Wallet::walletOpened, this, [this](bool status) {
619                 d->slotWalletOpened(status);
620             });
621             d->walletAsyncOpen = true;
622         } else {
623             d->walletOpenFailed = true;
624             loadPasswords();
625         }
626         return;
627     }
628     if (d->wallet && !d->walletAsyncOpen) {
629         loadPasswords();
630     }
631 }
632 
slotWalletOpened(bool success)633 void TransportManagerPrivate::slotWalletOpened(bool success)
634 {
635     qCDebug(MAILTRANSPORT_LOG);
636     walletAsyncOpen = false;
637     if (!success) {
638         walletOpenFailed = true;
639         delete wallet;
640         wallet = nullptr;
641     } else {
642         prepareWallet();
643     }
644     q->loadPasswords();
645 }
646 
validateDefault()647 void TransportManagerPrivate::validateDefault()
648 {
649     if (!q->transportById(defaultTransportId, false)) {
650         if (q->isEmpty()) {
651             defaultTransportId = -1;
652         } else {
653             defaultTransportId = transports.constFirst()->id();
654             writeConfig();
655         }
656     }
657 }
658 
migrateToWallet()659 void TransportManagerPrivate::migrateToWallet()
660 {
661     // check if we tried this already
662     static bool firstRun = true;
663     if (!firstRun) {
664         return;
665     }
666     firstRun = false;
667 
668     // check if we are the main instance
669     if (!isMainInstance) {
670         return;
671     }
672 
673     // check if migration is needed
674     QStringList names;
675     for (Transport *t : std::as_const(transports)) {
676         if (t->needsWalletMigration()) {
677             names << t->name();
678         }
679     }
680     if (names.isEmpty()) {
681         return;
682     }
683 
684     // ask user if he wants to migrate
685     int result = KMessageBox::questionYesNoList(nullptr,
686                                                 i18n("The following mail transports store their passwords in an "
687                                                      "unencrypted configuration file.\nFor security reasons, "
688                                                      "please consider migrating these passwords to KWallet, the "
689                                                      "KDE Wallet management tool,\nwhich stores sensitive data "
690                                                      "for you in a strongly encrypted file.\n"
691                                                      "Do you want to migrate your passwords to KWallet?"),
692                                                 names,
693                                                 i18n("Question"),
694                                                 KGuiItem(i18n("Migrate")),
695                                                 KGuiItem(i18n("Keep")),
696                                                 QStringLiteral("WalletMigrate"));
697     if (result != KMessageBox::Yes) {
698         return;
699     }
700 
701     // perform migration
702     for (Transport *t : std::as_const(transports)) {
703         if (t->needsWalletMigration()) {
704             t->migrateToWallet();
705         }
706     }
707 }
708 
dbusServiceUnregistered()709 void TransportManagerPrivate::dbusServiceUnregistered()
710 {
711     QDBusConnection::sessionBus().registerService(DBUS_SERVICE_NAME);
712 }
713 
jobResult(KJob * job)714 void TransportManagerPrivate::jobResult(KJob *job)
715 {
716     walletQueue.removeAll(static_cast<TransportJob *>(job));
717 }
718 
719 #include "moc_transportmanager.cpp"
720