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