MailTransport

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

KDE's Doxygen guidelines are available online.