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 "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;
41 
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, [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:
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 
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 
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 
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 
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 
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 
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(),
205  }
206  connect(promise, &KGAPI2::AccountPromise::finished, this, &SmtpJob::onTokenRequestFinished);
207  });
208  } else {
209  startLoginJob();
210  }
211 }
212 
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 
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) {
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 
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->addSubjob(login);
312  login->start();
313  qCDebug(MAILTRANSPORT_SMTP_LOG) << "Login started";
314 }
315 
316 void SmtpJob::startSendJob()
317 {
318  auto send = new KSmtp::SendJob(d->session);
319  send->setFrom(sender());
320  send->setTo(to());
321  send->setCc(cc());
322  send->setBcc(bcc());
323  send->setData(data());
324  send->setDeliveryStatusNotification(deliveryStatusNotification());
325 
326  addSubjob(send);
327  send->start();
328 
329  qCDebug(MAILTRANSPORT_SMTP_LOG) << "Send started";
330 }
331 
332 bool SmtpJob::doKill()
333 {
334  if (s_sessionPool.isDestroyed()) {
335  return false;
336  }
337 
338  if (!hasSubjobs()) {
339  return true;
340  }
341  if (d->currentState == SmtpJobPrivate::Precommand) {
342  return subjobs().first()->kill();
343  } else if (d->currentState == SmtpJobPrivate::Smtp) {
344  clearSubjobs();
345  s_sessionPool->removeSession(d->session);
346  return true;
347  }
348  return false;
349 }
350 
351 void SmtpJob::slotResult(KJob *job)
352 {
353  if (s_sessionPool.isDestroyed()) {
354  removeSubjob(job);
355  return;
356  }
357  if (qobject_cast<KSmtp::LoginJob *>(job)) {
358  if (job->error() == KSmtp::LoginJob::TokenExpired) {
359  removeSubjob(job);
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);
394  return;
395  }
396 
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"
QStringList bcc() const
Returns the "Bcc" receiver(s) of the mail.
Abstract base class for all mail transport jobs.
Definition: transportjob.h:30
virtual void reject()
void setErrorText(const QString &errorText)
void clearSubjobs()
void result(KJob *job)
void ref()
bool deliveryStatusNotification() const
Returns true if DSN is enabled.
void finished(KGAPI2::AccountPromise *self)
QByteArray data() const
Returns the data of the mail.
virtual bool removeSubjob(KJob *job)
QStringList to() const
Returns the "To" receiver(s) of the mail.
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
Q_GLOBAL_STATIC(Internal::StaticControl, s_instance) class ControlPrivate
Transport * transport() const
Returns the Transport object containing the mail transport settings.
const QList< KJob * > & subjobs() const
QStringList cc() const
Returns the "Cc" receiver(s) of the mail.
void setPassword(const QString &passwd)
Sets the password of this transport.
Definition: transport.cpp:58
Job to execute a command.
Definition: precommandjob.h:31
void deleteLater()
State
void doStart() override
Do the actual work, implement in your subclass.
Definition: smtpjob.cpp:107
QString sender() const
Returns the sender of the mail.
QString i18n(const char *text, const TYPE &arg...)
const Key key(const T &value) const const
virtual bool addSubjob(KJob *job)
UniqueConnection
int indexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
KCONFIGCORE_EXPORT bool authorize(const QString &action)
Represents the settings of a specific mail transport.
Definition: transport.h:32
Mail transport job for SMTP.
Definition: smtpjob.h:42
SmtpJob(Transport *transport, QObject *parent=nullptr)
Creates a SmtpJob.
Definition: smtpjob.cpp:81
ScriptableExtension * host() const
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
int remove(const Key &key)
QString left(int n) const const
QString name(StandardShortcut id)
virtual void slotResult(KJob *job)
void emitResult()
~SmtpJob() override
Deletes this job.
Definition: smtpjob.cpp:94
int error() const
bool hasSubjobs() const
static QUrl mailScopeUrl()
void setError(int errorCode)
WA_DeleteOnClose
void finished(int result)
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Thu Dec 7 2023 03:53:04 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.