MailTransport

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

KDE's Doxygen guidelines are available online.