1 /*
2   SPDX-FileCopyrightText: 2007 Volker Krause <vkrause@kde.org>
3 
4   Based on KMail code by:
5   SPDX-FileCopyrightText: 1996-1998 Stefan Taferner <taferner@kde.org>
6 
7   SPDX-License-Identifier: LGPL-2.0-or-later
8 */
9 
10 #include "smtpjob.h"
11 #include "mailtransport_defs.h"
12 #include "mailtransportplugin_smtp_debug.h"
13 #include "precommandjob.h"
14 #include "sessionuiproxy.h"
15 #include "transport.h"
16 #include <KAuthorized>
17 #include <QHash>
18 #include <QPointer>
19 
20 #include "mailtransport_debug.h"
21 #include <KLocalizedString>
22 #include <KPasswordDialog>
23 
24 #include <KSMTP/LoginJob>
25 #include <KSMTP/SendJob>
26 
27 #include <KGAPI/Account>
28 #include <KGAPI/AccountManager>
29 #include <KGAPI/AuthJob>
30 
31 #define GOOGLE_API_KEY QStringLiteral("554041944266.apps.googleusercontent.com")
32 #define GOOGLE_API_SECRET QStringLiteral("mdT1DjzohxN3npUUzkENT0gO")
33 
34 using namespace MailTransport;
35 
36 class SessionPool
37 {
38 public:
39     int ref = 0;
40     QHash<int, KSmtp::Session *> sessions;
41 
removeSession(KSmtp::Session * session)42     void removeSession(KSmtp::Session *session)
43     {
44         qCDebug(MAILTRANSPORT_SMTP_LOG) << "Removing session" << session << "from the pool";
45         int key = sessions.key(session);
46         if (key > 0) {
47             QObject::connect(session, &KSmtp::Session::stateChanged, [session](KSmtp::Session::State state) {
48                 if (state == KSmtp::Session::Disconnected) {
49                     session->deleteLater();
50                 }
51             });
52             session->quit();
53             sessions.remove(key);
54         }
55     }
56 };
57 
58 Q_GLOBAL_STATIC(SessionPool, s_sessionPool)
59 
60 /**
61  * Private class that helps to provide binary compatibility between releases.
62  * @internal
63  */
64 class SmtpJobPrivate
65 {
66 public:
SmtpJobPrivate(SmtpJob * parent)67     explicit SmtpJobPrivate(SmtpJob *parent)
68         : q(parent)
69     {
70     }
71 
72     void doLogin();
73 
74     SmtpJob *const q;
75     KSmtp::Session *session = nullptr;
76     KSmtp::SessionUiProxy::Ptr uiProxy;
77     enum State { Idle, Precommand, Smtp } currentState;
78     bool finished;
79 };
80 
SmtpJob(Transport * transport,QObject * parent)81 SmtpJob::SmtpJob(Transport *transport, QObject *parent)
82     : TransportJob(transport, parent)
83     , d(new SmtpJobPrivate(this))
84 {
85     d->currentState = SmtpJobPrivate::Idle;
86     d->session = nullptr;
87     d->finished = false;
88     d->uiProxy = KSmtp::SessionUiProxy::Ptr(new SmtpSessionUiProxy);
89     if (!s_sessionPool.isDestroyed()) {
90         s_sessionPool->ref++;
91     }
92 }
93 
~SmtpJob()94 SmtpJob::~SmtpJob()
95 {
96     if (!s_sessionPool.isDestroyed()) {
97         s_sessionPool->ref--;
98         if (s_sessionPool->ref == 0) {
99             qCDebug(MAILTRANSPORT_SMTP_LOG) << "clearing SMTP session pool" << s_sessionPool->sessions.count();
100             while (!s_sessionPool->sessions.isEmpty()) {
101                 s_sessionPool->removeSession(*(s_sessionPool->sessions.begin()));
102             }
103         }
104     }
105 }
106 
doStart()107 void SmtpJob::doStart()
108 {
109     if (s_sessionPool.isDestroyed()) {
110         return;
111     }
112 
113     if ((!s_sessionPool->sessions.isEmpty() && s_sessionPool->sessions.contains(transport()->id())) || transport()->precommand().isEmpty()) {
114         d->currentState = SmtpJobPrivate::Smtp;
115         startSmtpJob();
116     } else {
117         d->currentState = SmtpJobPrivate::Precommand;
118         auto job = new PrecommandJob(transport()->precommand(), this);
119         addSubjob(job);
120         job->start();
121     }
122 }
123 
startSmtpJob()124 void SmtpJob::startSmtpJob()
125 {
126     if (s_sessionPool.isDestroyed()) {
127         return;
128     }
129 
130     d->session = s_sessionPool->sessions.value(transport()->id());
131     if (!d->session) {
132         d->session = new KSmtp::Session(transport()->host(), transport()->port());
133         d->session->setUseNetworkProxy(transport()->useProxy());
134         d->session->setUiProxy(d->uiProxy);
135         switch (transport()->encryption()) {
136         case Transport::EnumEncryption::None:
137             d->session->setEncryptionMode(KSmtp::Session::Unencrypted);
138             break;
139         case Transport::EnumEncryption::TLS:
140             d->session->setEncryptionMode(KSmtp::Session::STARTTLS);
141             break;
142         case Transport::EnumEncryption::SSL:
143             d->session->setEncryptionMode(KSmtp::Session::TLS);
144             break;
145         default:
146             qCWarning(MAILTRANSPORT_SMTP_LOG) << "Unknown encryption mode" << transport()->encryption();
147             break;
148         }
149         if (transport()->specifyHostname()) {
150             d->session->setCustomHostname(transport()->localHostname());
151         }
152         s_sessionPool->sessions.insert(transport()->id(), d->session);
153     }
154 
155     connect(d->session, &KSmtp::Session::stateChanged, this, &SmtpJob::sessionStateChanged, Qt::UniqueConnection);
156     connect(d->session, &KSmtp::Session::connectionError, this, [this](const QString &err) {
157         setError(KJob::UserDefinedError);
158         setErrorText(err);
159         s_sessionPool->removeSession(d->session);
160         emitResult();
161     });
162 
163     if (d->session->state() == KSmtp::Session::Disconnected) {
164         d->session->open();
165     } else {
166         if (d->session->state() != KSmtp::Session::Authenticated) {
167             startPasswordRetrieval();
168         }
169 
170         startSendJob();
171     }
172 }
173 
sessionStateChanged(KSmtp::Session::State state)174 void SmtpJob::sessionStateChanged(KSmtp::Session::State state)
175 {
176     if (state == KSmtp::Session::Ready) {
177         startPasswordRetrieval();
178     } else if (state == KSmtp::Session::Authenticated) {
179         startSendJob();
180     }
181 }
182 
startPasswordRetrieval(bool forceRefresh)183 void SmtpJob::startPasswordRetrieval(bool forceRefresh)
184 {
185     if (!transport()->requiresAuthentication() && !forceRefresh) {
186         startSendJob();
187         return;
188     }
189 
190     if (transport()->authenticationType() == TransportBase::EnumAuthenticationType::XOAUTH2) {
191         auto promise = KGAPI2::AccountManager::instance()->findAccount(GOOGLE_API_KEY, transport()->userName(), {KGAPI2::Account::mailScopeUrl()});
192         connect(promise, &KGAPI2::AccountPromise::finished, this, [forceRefresh, this](KGAPI2::AccountPromise *promise) {
193             if (promise->account()) {
194                 if (forceRefresh) {
195                     promise = KGAPI2::AccountManager::instance()->refreshTokens(GOOGLE_API_KEY, GOOGLE_API_SECRET, transport()->userName());
196                 } else {
197                     onTokenRequestFinished(promise);
198                     return;
199                 }
200             } else {
201                 promise = KGAPI2::AccountManager::instance()->getAccount(GOOGLE_API_KEY,
202                                                                          GOOGLE_API_SECRET,
203                                                                          transport()->userName(),
204                                                                          {KGAPI2::Account::mailScopeUrl()});
205             }
206             connect(promise, &KGAPI2::AccountPromise::finished, this, &SmtpJob::onTokenRequestFinished);
207         });
208     } else {
209         startLoginJob();
210     }
211 }
212 
onTokenRequestFinished(KGAPI2::AccountPromise * promise)213 void SmtpJob::onTokenRequestFinished(KGAPI2::AccountPromise *promise)
214 {
215     if (promise->hasError()) {
216         qCWarning(MAILTRANSPORT_SMTP_LOG) << "Error obtaining XOAUTH2 token:" << promise->errorText();
217         setError(KJob::UserDefinedError);
218         setErrorText(promise->errorText());
219         emitResult();
220         return;
221     }
222 
223     const auto account = promise->account();
224     const QString tokens = QStringLiteral("%1\001%2").arg(account->accessToken(), account->refreshToken());
225     transport()->setPassword(tokens);
226     startLoginJob();
227 }
228 
startLoginJob()229 void SmtpJob::startLoginJob()
230 {
231     if (!transport()->requiresAuthentication()) {
232         startSendJob();
233         return;
234     }
235 
236     auto user = transport()->userName();
237     auto passwd = transport()->password();
238     if ((user.isEmpty() || passwd.isEmpty()) && transport()->authenticationType() != Transport::EnumAuthenticationType::GSSAPI) {
239         QPointer<KPasswordDialog> dlg = new KPasswordDialog(nullptr, KPasswordDialog::ShowUsernameLine | KPasswordDialog::ShowKeepPassword);
240         dlg->setAttribute(Qt::WA_DeleteOnClose, true);
241         dlg->setPrompt(
242             i18n("You need to supply a username and a password "
243                  "to use this SMTP server."));
244         dlg->setKeepPassword(transport()->storePassword());
245         dlg->addCommentLine(QString(), transport()->name());
246         dlg->setUsername(user);
247         dlg->setPassword(passwd);
248         dlg->setRevealPasswordAvailable(KAuthorized::authorize(QStringLiteral("lineedit_reveal_password")));
249 
250         connect(this, &KJob::result, dlg, &QDialog::reject);
251 
252         connect(dlg, &QDialog::finished, this, [this, dlg](const int result) {
253             if (result == QDialog::Rejected) {
254                 setError(KilledJobError);
255                 emitResult();
256                 return;
257             }
258 
259             transport()->setUserName(dlg->username());
260             transport()->setPassword(dlg->password());
261             transport()->setStorePassword(dlg->keepPassword());
262             transport()->save();
263 
264             d->doLogin();
265         });
266         dlg->open();
267 
268         return;
269     }
270 
271     d->doLogin();
272 }
273 
doLogin()274 void SmtpJobPrivate::doLogin()
275 {
276     QString passwd = q->transport()->password();
277     if (q->transport()->authenticationType() == Transport::EnumAuthenticationType::XOAUTH2) {
278         passwd = passwd.left(passwd.indexOf(QLatin1Char('\001')));
279     }
280 
281     auto login = new KSmtp::LoginJob(session);
282     login->setUserName(q->transport()->userName());
283     login->setPassword(passwd);
284     switch (q->transport()->authenticationType()) {
285     case TransportBase::EnumAuthenticationType::PLAIN:
286         login->setPreferedAuthMode(KSmtp::LoginJob::Plain);
287         break;
288     case TransportBase::EnumAuthenticationType::LOGIN:
289         login->setPreferedAuthMode(KSmtp::LoginJob::Login);
290         break;
291     case TransportBase::EnumAuthenticationType::CRAM_MD5:
292         login->setPreferedAuthMode(KSmtp::LoginJob::CramMD5);
293         break;
294     case TransportBase::EnumAuthenticationType::XOAUTH2:
295         login->setPreferedAuthMode(KSmtp::LoginJob::XOAuth2);
296         break;
297     case TransportBase::EnumAuthenticationType::DIGEST_MD5:
298         login->setPreferedAuthMode(KSmtp::LoginJob::DigestMD5);
299         break;
300     case TransportBase::EnumAuthenticationType::NTLM:
301         login->setPreferedAuthMode(KSmtp::LoginJob::NTLM);
302         break;
303     case TransportBase::EnumAuthenticationType::GSSAPI:
304         login->setPreferedAuthMode(KSmtp::LoginJob::GSSAPI);
305         break;
306     default:
307         qCWarning(MAILTRANSPORT_SMTP_LOG) << "Unknown authentication mode" << q->transport()->authenticationTypeString();
308         break;
309     }
310 
311     q->connect(login, &KJob::result, q, &SmtpJob::slotResult);
312     q->addSubjob(login);
313     login->start();
314     qCDebug(MAILTRANSPORT_SMTP_LOG) << "Login started";
315 }
316 
startSendJob()317 void SmtpJob::startSendJob()
318 {
319     auto send = new KSmtp::SendJob(d->session);
320     send->setFrom(sender());
321     send->setTo(to());
322     send->setCc(cc());
323     send->setBcc(bcc());
324     send->setData(data());
325     send->setDeliveryStatusNotification(deliveryStatusNotification());
326 
327     addSubjob(send);
328     send->start();
329 
330     qCDebug(MAILTRANSPORT_SMTP_LOG) << "Send started";
331 }
332 
doKill()333 bool SmtpJob::doKill()
334 {
335     if (s_sessionPool.isDestroyed()) {
336         return false;
337     }
338 
339     if (!hasSubjobs()) {
340         return true;
341     }
342     if (d->currentState == SmtpJobPrivate::Precommand) {
343         return subjobs().first()->kill();
344     } else if (d->currentState == SmtpJobPrivate::Smtp) {
345         clearSubjobs();
346         s_sessionPool->removeSession(d->session);
347         return true;
348     }
349     return false;
350 }
351 
slotResult(KJob * job)352 void SmtpJob::slotResult(KJob *job)
353 {
354     if (s_sessionPool.isDestroyed()) {
355         return;
356     }
357 
358     if (qobject_cast<KSmtp::LoginJob *>(job)) {
359         if (job->error() == KSmtp::LoginJob::TokenExpired) {
360             startPasswordRetrieval(/*force refresh */ true);
361             return;
362         }
363     }
364 
365     // The job has finished, so we don't care about any further errors. Set
366     // d->finished to true, so slaveError() knows about this and doesn't call
367     // emitResult() anymore.
368     // Sometimes, the SMTP slave emits more than one error
369     //
370     // The first error causes slotResult() to be called, but not slaveError(), since
371     // the scheduler doesn't emit errors for connected slaves.
372     //
373     // The second error then causes slaveError() to be called (as the slave is no
374     // longer connected), which does emitResult() a second time, which is invalid
375     // (and triggers an assert in KMail).
376     d->finished = true;
377 
378     // Normally, calling TransportJob::slotResult() would set the proper error code
379     // for error() via KComposite::slotResult(). However, we can't call that here,
380     // since that also emits the result signal.
381     // In KMail, when there are multiple mails in the outbox, KMail tries to send
382     // the next mail when it gets the result signal, which then would reuse the
383     // old broken slave from the slave pool if there was an error.
384     // To prevent that, we call TransportJob::slotResult() only after removing the
385     // slave from the pool and calculate the error code ourselves.
386     int errorCode = error();
387     if (!errorCode) {
388         errorCode = job->error();
389     }
390 
391     if (errorCode && d->currentState == SmtpJobPrivate::Smtp) {
392         s_sessionPool->removeSession(d->session);
393         TransportJob::slotResult(job);
394         return;
395     }
396 
397     TransportJob::slotResult(job);
398     if (!error() && d->currentState == SmtpJobPrivate::Precommand) {
399         d->currentState = SmtpJobPrivate::Smtp;
400         startSmtpJob();
401         return;
402     }
403     if (!error() && !hasSubjobs()) {
404         emitResult();
405     }
406 }
407 
408 #include "moc_smtpjob.cpp"
409