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