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 {
76 Idle,
77 Precommand,
78 Smtp
79 } currentState;
80 bool finished;
81};
82
84 : TransportJob(transport, parent)
85 , d(new SmtpJobPrivate(this))
86{
87 d->currentState = SmtpJobPrivate::Idle;
88 d->session = nullptr;
89 d->finished = false;
90 d->uiProxy = KSmtp::SessionUiProxy::Ptr(new SmtpSessionUiProxy);
91 if (!s_sessionPool.isDestroyed()) {
92 s_sessionPool->ref++;
93 }
94}
95
97{
98 if (!s_sessionPool.isDestroyed()) {
99 s_sessionPool->ref--;
100 if (s_sessionPool->ref == 0) {
101 qCDebug(MAILTRANSPORT_SMTP_LOG) << "clearing SMTP session pool" << s_sessionPool->sessions.count();
102 while (!s_sessionPool->sessions.isEmpty()) {
103 s_sessionPool->removeSession(*(s_sessionPool->sessions.begin()));
104 }
105 }
106 }
107}
108
110{
111 if (s_sessionPool.isDestroyed()) {
112 return;
113 }
114
115 if ((!s_sessionPool->sessions.isEmpty() && s_sessionPool->sessions.contains(transport()->id())) || transport()->precommand().isEmpty()) {
116 d->currentState = SmtpJobPrivate::Smtp;
117 startSmtpJob();
118 } else {
119 d->currentState = SmtpJobPrivate::Precommand;
120 auto job = new PrecommandJob(transport()->precommand(), this);
121 addSubjob(job);
122 job->start();
123 }
124}
125
126void SmtpJob::startSmtpJob()
127{
128 if (s_sessionPool.isDestroyed()) {
129 return;
130 }
131
132 d->session = s_sessionPool->sessions.value(transport()->id());
133 if (!d->session) {
134 d->session = new KSmtp::Session(transport()->host(), transport()->port());
135 d->session->setUseNetworkProxy(transport()->useProxy());
136 d->session->setUiProxy(d->uiProxy);
137 switch (transport()->encryption()) {
138 case Transport::EnumEncryption::None:
139 d->session->setEncryptionMode(KSmtp::Session::Unencrypted);
140 break;
141 case Transport::EnumEncryption::TLS:
142 d->session->setEncryptionMode(KSmtp::Session::STARTTLS);
143 break;
144 case Transport::EnumEncryption::SSL:
145 d->session->setEncryptionMode(KSmtp::Session::TLS);
146 break;
147 default:
148 qCWarning(MAILTRANSPORT_SMTP_LOG) << "Unknown encryption mode" << transport()->encryption();
149 break;
150 }
151 if (transport()->specifyHostname()) {
152 d->session->setCustomHostname(transport()->localHostname());
153 }
154 s_sessionPool->sessions.insert(transport()->id(), d->session);
155 }
156
157 connect(d->session, &KSmtp::Session::stateChanged, this, &SmtpJob::sessionStateChanged, Qt::UniqueConnection);
158 connect(d->session, &KSmtp::Session::connectionError, this, [this](const QString &err) {
159 setError(KJob::UserDefinedError);
160 setErrorText(err);
161 s_sessionPool->removeSession(d->session);
162 emitResult();
163 });
164
165 if (d->session->state() == KSmtp::Session::Disconnected) {
166 d->session->open();
167 } else {
168 if (d->session->state() != KSmtp::Session::Authenticated) {
169 startPasswordRetrieval();
170 }
171
172 startSendJob();
173 }
174}
175
176void SmtpJob::sessionStateChanged(KSmtp::Session::State state)
177{
178 if (state == KSmtp::Session::Ready) {
179 startPasswordRetrieval();
180 } else if (state == KSmtp::Session::Authenticated) {
181 startSendJob();
182 }
183}
184
185void SmtpJob::startPasswordRetrieval(bool forceRefresh)
186{
187 if (!transport()->requiresAuthentication() && !forceRefresh) {
188 startSendJob();
189 return;
190 }
191
192 auto xoauthRequester = createXOAuthPasswordRequester(transport(), this);
193 if (xoauthRequester != nullptr) {
194 connect(xoauthRequester,
195 &XOAuthPasswordRequester::done,
196 this,
197 [this, xoauthRequester](XOAuthPasswordRequester::Result result, const QString &password) {
198 xoauthRequester->deleteLater();
199 if (result == XOAuthPasswordRequester::Error) {
200 setError(KJob::UserDefinedError);
201 emitResult();
202 return;
203 }
204
205 transport()->setPassword(password);
206 startLoginJob();
207 });
208 xoauthRequester->requestPassword(forceRefresh);
209 } else {
210 startLoginJob();
211 }
212}
213
214void SmtpJob::startLoginJob()
215{
216 if (!transport()->requiresAuthentication()) {
217 startSendJob();
218 return;
219 }
220
221 auto user = transport()->userName();
222 auto passwd = transport()->password();
223 if ((user.isEmpty() || passwd.isEmpty()) && transport()->authenticationType() != Transport::EnumAuthenticationType::GSSAPI) {
225 dlg->setAttribute(Qt::WA_DeleteOnClose, true);
226 dlg->setPrompt(
227 i18n("You need to supply a username and a password "
228 "to use this SMTP server."));
229 dlg->setKeepPassword(transport()->storePassword());
230 dlg->addCommentLine(QString(), transport()->name());
231 dlg->setUsername(user);
232 dlg->setPassword(passwd);
233 dlg->setRevealPasswordMode(KAuthorized::authorize(QStringLiteral("lineedit_reveal_password")) ? KPassword::RevealMode::OnlyNew
234 : KPassword::RevealMode::Never);
235
236 connect(this, &KJob::result, dlg, &QDialog::reject);
237
238 connect(dlg, &QDialog::finished, this, [this, dlg](const int result) {
239 if (result == QDialog::Rejected) {
240 setError(KilledJobError);
241 emitResult();
242 return;
243 }
244
245 transport()->setUserName(dlg->username());
246 transport()->setPassword(dlg->password());
247 transport()->setStorePassword(dlg->keepPassword());
248 transport()->save();
249
250 d->doLogin();
251 });
252 dlg->open();
253
254 return;
255 }
256
257 d->doLogin();
258}
259
260void SmtpJobPrivate::doLogin()
261{
262 QString passwd = q->transport()->password();
263 if (q->transport()->authenticationType() == Transport::EnumAuthenticationType::XOAUTH2) {
264 passwd = passwd.left(passwd.indexOf(QLatin1Char('\001')));
265 }
266
267 auto login = new KSmtp::LoginJob(session);
268 login->setUserName(q->transport()->userName());
269 login->setPassword(passwd);
270 switch (q->transport()->authenticationType()) {
271 case TransportBase::EnumAuthenticationType::PLAIN:
272 login->setPreferedAuthMode(KSmtp::LoginJob::Plain);
273 break;
274 case TransportBase::EnumAuthenticationType::LOGIN:
275 login->setPreferedAuthMode(KSmtp::LoginJob::Login);
276 break;
277 case TransportBase::EnumAuthenticationType::CRAM_MD5:
278 login->setPreferedAuthMode(KSmtp::LoginJob::CramMD5);
279 break;
280 case TransportBase::EnumAuthenticationType::XOAUTH2:
281 login->setPreferedAuthMode(KSmtp::LoginJob::XOAuth2);
282 break;
283 case TransportBase::EnumAuthenticationType::DIGEST_MD5:
284 login->setPreferedAuthMode(KSmtp::LoginJob::DigestMD5);
285 break;
286 case TransportBase::EnumAuthenticationType::NTLM:
287 login->setPreferedAuthMode(KSmtp::LoginJob::NTLM);
288 break;
289 case TransportBase::EnumAuthenticationType::GSSAPI:
290 login->setPreferedAuthMode(KSmtp::LoginJob::GSSAPI);
291 break;
292 default:
293 qCWarning(MAILTRANSPORT_SMTP_LOG) << "Unknown authentication mode" << q->transport()->authenticationTypeString();
294 break;
295 }
296
297 q->addSubjob(login);
298 login->start();
299 qCDebug(MAILTRANSPORT_SMTP_LOG) << "Login started";
300}
301
302void SmtpJob::startSendJob()
303{
304 auto send = new KSmtp::SendJob(d->session);
305 send->setFrom(sender());
306 send->setTo(to());
307 send->setCc(cc());
308 send->setBcc(bcc());
309 send->setData(data());
310 send->setDeliveryStatusNotification(deliveryStatusNotification());
311
312 addSubjob(send);
313 send->start();
314
315 qCDebug(MAILTRANSPORT_SMTP_LOG) << "Send started";
316}
317
318bool SmtpJob::doKill()
319{
320 if (s_sessionPool.isDestroyed()) {
321 return false;
322 }
323
324 if (!hasSubjobs()) {
325 return true;
326 }
327 if (d->currentState == SmtpJobPrivate::Precommand) {
328 return subjobs().first()->kill();
329 } else if (d->currentState == SmtpJobPrivate::Smtp) {
330 clearSubjobs();
331 s_sessionPool->removeSession(d->session);
332 return true;
333 }
334 return false;
335}
336
337void SmtpJob::slotResult(KJob *job)
338{
339 if (s_sessionPool.isDestroyed()) {
340 removeSubjob(job);
341 return;
342 }
344 if (job->error() == KSmtp::LoginJob::TokenExpired) {
345 removeSubjob(job);
346 startPasswordRetrieval(/*force refresh */ true);
347 return;
348 }
349 }
350
351 // The job has finished, so we don't care about any further errors. Set
352 // d->finished to true, so slaveError() knows about this and doesn't call
353 // emitResult() anymore.
354 // Sometimes, the SMTP slave emits more than one error
355 //
356 // The first error causes slotResult() to be called, but not slaveError(), since
357 // the scheduler doesn't emit errors for connected slaves.
358 //
359 // The second error then causes slaveError() to be called (as the slave is no
360 // longer connected), which does emitResult() a second time, which is invalid
361 // (and triggers an assert in KMail).
362 d->finished = true;
363
364 // Normally, calling TransportJob::slotResult() would set the proper error code
365 // for error() via KComposite::slotResult(). However, we can't call that here,
366 // since that also emits the result signal.
367 // In KMail, when there are multiple mails in the outbox, KMail tries to send
368 // the next mail when it gets the result signal, which then would reuse the
369 // old broken slave from the slave pool if there was an error.
370 // To prevent that, we call TransportJob::slotResult() only after removing the
371 // slave from the pool and calculate the error code ourselves.
372 int errorCode = error();
373 if (!errorCode) {
374 errorCode = job->error();
375 }
376
377 if (errorCode && d->currentState == SmtpJobPrivate::Smtp) {
378 s_sessionPool->removeSession(d->session);
380 return;
381 }
382
384 if (!error() && d->currentState == SmtpJobPrivate::Precommand) {
385 d->currentState = SmtpJobPrivate::Smtp;
386 startSmtpJob();
387 return;
388 }
389 if (!error() && !hasSubjobs()) {
390 emitResult();
391 }
392}
393
394#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:96
SmtpJob(Transport *transport, QObject *parent=nullptr)
Creates a SmtpJob.
Definition smtpjob.cpp:83
void doStart() override
Do the actual work, implement in your subclass.
Definition smtpjob.cpp:109
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(GameStandardAction 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 Fri Nov 22 2024 12:07:33 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.