smtpjob.cpp 13.1 KB
Newer Older
1
/*
2
  SPDX-FileCopyrightText: 2007 Volker Krause <vkrause@kde.org>
3

Allen Winter's avatar
Allen Winter committed
4
  Based on KMail code by:
5
6
7
  SPDX-FileCopyrightText: 1996-1998 Stefan Taferner <taferner@kde.org>

  SPDX-License-Identifier: LGPL-2.0-or-later
8
9
10
11
*/

#include "smtpjob.h"
#include "mailtransport_defs.h"
Laurent Montel's avatar
Laurent Montel committed
12
#include "mailtransportplugin_smtp_debug.h"
Volker Krause's avatar
Volker Krause committed
13
#include "precommandjob.h"
14
#include "sessionuiproxy.h"
Laurent Montel's avatar
Laurent Montel committed
15
#include "transport.h"
16
#include <KAuthorized>
17
#include <QHash>
18
#include <QPointer>
19

Laurent Montel's avatar
Laurent Montel committed
20
#include "mailtransport_debug.h"
Laurent Montel's avatar
Laurent Montel committed
21
#include <KLocalizedString>
22
#include <KPasswordDialog>
23

24
25
26
#include <KSMTP/LoginJob>
#include <KSMTP/SendJob>

27
#include <KGAPI/Account>
28
#include <KGAPI/AccountManager>
Laurent Montel's avatar
Laurent Montel committed
29
#include <KGAPI/AuthJob>
30
31
32
33

#define GOOGLE_API_KEY QStringLiteral("554041944266.apps.googleusercontent.com")
#define GOOGLE_API_SECRET QStringLiteral("mdT1DjzohxN3npUUzkENT0gO")

34
using namespace MailTransport;
35

36
class SessionPool
37
{
Laurent Montel's avatar
Laurent Montel committed
38
public:
Laurent Montel's avatar
Laurent Montel committed
39
    int ref = 0;
Laurent Montel's avatar
Laurent Montel committed
40
    QHash<int, KSmtp::Session *> sessions;
41

42
    void removeSession(KSmtp::Session *session)
43
    {
44
45
46
        qCDebug(MAILTRANSPORT_SMTP_LOG) << "Removing session" << session << "from the pool";
        int key = sessions.key(session);
        if (key > 0) {
Laurent Montel's avatar
Laurent Montel committed
47
            QObject::connect(session, &KSmtp::Session::stateChanged, [session](KSmtp::Session::State state) {
Laurent Montel's avatar
Laurent Montel committed
48
49
50
51
                if (state == KSmtp::Session::Disconnected) {
                    session->deleteLater();
                }
            });
52
53
            session->quit();
            sessions.remove(key);
54
        }
Allen Winter's avatar
Allen Winter committed
55
    }
56
57
};

58
Q_GLOBAL_STATIC(SessionPool, s_sessionPool)
59

Tom Albers's avatar
Tom Albers committed
60
61
62
63
64
65
/**
 * Private class that helps to provide binary compatibility between releases.
 * @internal
 */
class SmtpJobPrivate
{
Laurent Montel's avatar
Laurent Montel committed
66
public:
Laurent Montel's avatar
Laurent Montel committed
67
    explicit SmtpJobPrivate(SmtpJob *parent)
Laurent Montel's avatar
Laurent Montel committed
68
        : q(parent)
Laurent Montel's avatar
Laurent Montel committed
69
70
    {
    }
71

72
73
    void doLogin();

Laurent Montel's avatar
Laurent Montel committed
74
    SmtpJob *const q;
Laurent Montel's avatar
Laurent Montel committed
75
    KSmtp::Session *session = nullptr;
76
    KSmtp::SessionUiProxy::Ptr uiProxy;
Laurent Montel's avatar
Laurent Montel committed
77
    enum State { Idle, Precommand, Smtp } currentState;
78
    bool finished;
Tom Albers's avatar
Tom Albers committed
79
80
};

Laurent Montel's avatar
Laurent Montel committed
81
SmtpJob::SmtpJob(Transport *transport, QObject *parent)
Laurent Montel's avatar
Laurent Montel committed
82
83
    : TransportJob(transport, parent)
    , d(new SmtpJobPrivate(this))
84
{
Laurent Montel's avatar
Laurent Montel committed
85
    d->currentState = SmtpJobPrivate::Idle;
86
    d->session = nullptr;
Laurent Montel's avatar
Laurent Montel committed
87
    d->finished = false;
88
89
90
    d->uiProxy = KSmtp::SessionUiProxy::Ptr(new SmtpSessionUiProxy);
    if (!s_sessionPool.isDestroyed()) {
        s_sessionPool->ref++;
Laurent Montel's avatar
Laurent Montel committed
91
    }
92
93
94
95
}

SmtpJob::~SmtpJob()
{
96
97
98
99
100
101
    if (!s_sessionPool.isDestroyed()) {
        s_sessionPool->ref--;
        if (s_sessionPool->ref == 0) {
            qCDebug(MAILTRANSPORT_SMTP_LOG) << "clearing SMTP session pool" << s_sessionPool->sessions.count();
            while (!s_sessionPool->sessions.isEmpty()) {
                s_sessionPool->removeSession(*(s_sessionPool->sessions.begin()));
Laurent Montel's avatar
Laurent Montel committed
102
            }
103
        }
Allen Winter's avatar
Allen Winter committed
104
    }
105
106
}

107
void SmtpJob::doStart()
Volker Krause's avatar
Volker Krause committed
108
{
109
    if (s_sessionPool.isDestroyed()) {
Laurent Montel's avatar
Laurent Montel committed
110
111
112
        return;
    }

Laurent Montel's avatar
Laurent Montel committed
113
    if ((!s_sessionPool->sessions.isEmpty() && s_sessionPool->sessions.contains(transport()->id())) || transport()->precommand().isEmpty()) {
Laurent Montel's avatar
Laurent Montel committed
114
115
116
117
        d->currentState = SmtpJobPrivate::Smtp;
        startSmtpJob();
    } else {
        d->currentState = SmtpJobPrivate::Precommand;
Laurent Montel's avatar
Laurent Montel committed
118
        auto job = new PrecommandJob(transport()->precommand(), this);
Laurent Montel's avatar
Laurent Montel committed
119
120
121
        addSubjob(job);
        job->start();
    }
Volker Krause's avatar
Volker Krause committed
122
123
124
}

void SmtpJob::startSmtpJob()
125
{
126
    if (s_sessionPool.isDestroyed()) {
127
128
129
        return;
    }

130
131
132
    d->session = s_sessionPool->sessions.value(transport()->id());
    if (!d->session) {
        d->session = new KSmtp::Session(transport()->host(), transport()->port());
133
        d->session->setUseNetworkProxy(transport()->useProxy());
134
        d->session->setUiProxy(d->uiProxy);
135
136
137
138
139
140
141
142
143
144
145
146
147
148
        switch (transport()->encryption()) {
        case Transport::EnumEncryption::None:
            d->session->setEncryptionMode(KSmtp::Session::Unencrypted);
            break;
        case Transport::EnumEncryption::TLS:
            d->session->setEncryptionMode(KSmtp::Session::STARTTLS);
            break;
        case Transport::EnumEncryption::SSL:
            d->session->setEncryptionMode(KSmtp::Session::TLS);
            break;
        default:
            qCWarning(MAILTRANSPORT_SMTP_LOG) << "Unknown encryption mode" << transport()->encryption();
            break;
        }
149
150
151
152
153
        if (transport()->specifyHostname()) {
            d->session->setCustomHostname(transport()->localHostname());
        }
        s_sessionPool->sessions.insert(transport()->id(), d->session);
    }
Laurent Montel's avatar
Laurent Montel committed
154

Laurent Montel's avatar
Laurent Montel committed
155
156
    connect(d->session, &KSmtp::Session::stateChanged, this, &SmtpJob::sessionStateChanged, Qt::UniqueConnection);
    connect(d->session, &KSmtp::Session::connectionError, this, [this](const QString &err) {
Laurent Montel's avatar
Laurent Montel committed
157
158
159
160
161
        setError(KJob::UserDefinedError);
        setErrorText(err);
        s_sessionPool->removeSession(d->session);
        emitResult();
    });
Laurent Montel's avatar
Laurent Montel committed
162

163
164
165
166
    if (d->session->state() == KSmtp::Session::Disconnected) {
        d->session->open();
    } else {
        if (d->session->state() != KSmtp::Session::Authenticated) {
167
            startPasswordRetrieval();
168
169
170
        }

        startSendJob();
Laurent Montel's avatar
Laurent Montel committed
171
    }
172
173
174
175
176
}

void SmtpJob::sessionStateChanged(KSmtp::Session::State state)
{
    if (state == KSmtp::Session::Ready) {
177
        startPasswordRetrieval();
178
179
    } else if (state == KSmtp::Session::Authenticated) {
        startSendJob();
Laurent Montel's avatar
Laurent Montel committed
180
    }
181
}
Laurent Montel's avatar
Laurent Montel committed
182

183
void SmtpJob::startPasswordRetrieval(bool forceRefresh)
184
{
185
    if (!transport()->requiresAuthentication() && !forceRefresh) {
186
187
188
189
190
        startSendJob();
        return;
    }

    if (transport()->authenticationType() == TransportBase::EnumAuthenticationType::XOAUTH2) {
Laurent Montel's avatar
Laurent Montel committed
191
192
        auto promise = KGAPI2::AccountManager::instance()->findAccount(GOOGLE_API_KEY, transport()->userName(), {KGAPI2::Account::mailScopeUrl()});
        connect(promise, &KGAPI2::AccountPromise::finished, this, [forceRefresh, this](KGAPI2::AccountPromise *promise) {
Laurent Montel's avatar
Laurent Montel committed
193
194
            if (promise->account()) {
                if (forceRefresh) {
Laurent Montel's avatar
Laurent Montel committed
195
                    promise = KGAPI2::AccountManager::instance()->refreshTokens(GOOGLE_API_KEY, GOOGLE_API_SECRET, transport()->userName());
Laurent Montel's avatar
Laurent Montel committed
196
197
198
199
200
                } else {
                    onTokenRequestFinished(promise);
                    return;
                }
            } else {
Laurent Montel's avatar
Laurent Montel committed
201
202
203
204
                promise = KGAPI2::AccountManager::instance()->getAccount(GOOGLE_API_KEY,
                                                                         GOOGLE_API_SECRET,
                                                                         transport()->userName(),
                                                                         {KGAPI2::Account::mailScopeUrl()});
Laurent Montel's avatar
Laurent Montel committed
205
            }
Laurent Montel's avatar
Laurent Montel committed
206
            connect(promise, &KGAPI2::AccountPromise::finished, this, &SmtpJob::onTokenRequestFinished);
Laurent Montel's avatar
Laurent Montel committed
207
        });
208
209
210
211
212
    } else {
        startLoginJob();
    }
}

213
void SmtpJob::onTokenRequestFinished(KGAPI2::AccountPromise *promise)
214
{
215
216
217
218
219
    if (promise->hasError()) {
        qCWarning(MAILTRANSPORT_SMTP_LOG) << "Error obtaining XOAUTH2 token:" << promise->errorText();
        setError(KJob::UserDefinedError);
        setErrorText(promise->errorText());
        emitResult();
220
221
222
        return;
    }

223
    const auto account = promise->account();
Laurent Montel's avatar
Laurent Montel committed
224
    const QString tokens = QStringLiteral("%1\001%2").arg(account->accessToken(), account->refreshToken());
225
226
227
228
    transport()->setPassword(tokens);
    startLoginJob();
}

229
230
231
232
233
void SmtpJob::startLoginJob()
{
    if (!transport()->requiresAuthentication()) {
        startSendJob();
        return;
Laurent Montel's avatar
Laurent Montel committed
234
    }
235

236
237
    auto user = transport()->userName();
    auto passwd = transport()->password();
Laurent Montel's avatar
Laurent Montel committed
238
239
    if ((user.isEmpty() || passwd.isEmpty()) && transport()->authenticationType() != Transport::EnumAuthenticationType::GSSAPI) {
        QPointer<KPasswordDialog> dlg = new KPasswordDialog(nullptr, KPasswordDialog::ShowUsernameLine | KPasswordDialog::ShowKeepPassword);
240
        dlg->setAttribute(Qt::WA_DeleteOnClose, true);
Laurent Montel's avatar
Laurent Montel committed
241
242
243
        dlg->setPrompt(
            i18n("You need to supply a username and a password "
                 "to use this SMTP server."));
244
245
246
247
        dlg->setKeepPassword(transport()->storePassword());
        dlg->addCommentLine(QString(), transport()->name());
        dlg->setUsername(user);
        dlg->setPassword(passwd);
248
        dlg->setRevealPasswordAvailable(KAuthorized::authorize(QStringLiteral("lineedit_reveal_password")));
249

250
251
252
253
254
255
256
257
258
        connect(this, &KJob::result, dlg, &QDialog::reject);

        connect(dlg, &QDialog::finished, this, [this, dlg](const int result) {
            if (result == QDialog::Rejected) {
                setError(KilledJobError);
                emitResult();
                return;
            }

259
260
261
262
            transport()->setUserName(dlg->username());
            transport()->setPassword(dlg->password());
            transport()->setStorePassword(dlg->keepPassword());
            transport()->save();
Laurent Montel's avatar
Laurent Montel committed
263

264
265
266
267
268
            d->doLogin();
        });
        dlg->open();

        return;
Laurent Montel's avatar
Laurent Montel committed
269
270
    }

271
272
    d->doLogin();
}
273

274
275
276
277
void SmtpJobPrivate::doLogin()
{
    QString passwd = q->transport()->password();
    if (q->transport()->authenticationType() == Transport::EnumAuthenticationType::XOAUTH2) {
278
279
        passwd = passwd.left(passwd.indexOf(QLatin1Char('\001')));
    }
280

281
282
    auto login = new KSmtp::LoginJob(session);
    login->setUserName(q->transport()->userName());
Laurent Montel's avatar
Laurent Montel committed
283
    login->setPassword(passwd);
284
    switch (q->transport()->authenticationType()) {
285
286
287
288
289
290
291
292
293
294
    case TransportBase::EnumAuthenticationType::PLAIN:
        login->setPreferedAuthMode(KSmtp::LoginJob::Plain);
        break;
    case TransportBase::EnumAuthenticationType::LOGIN:
        login->setPreferedAuthMode(KSmtp::LoginJob::Login);
        break;
    case TransportBase::EnumAuthenticationType::CRAM_MD5:
        login->setPreferedAuthMode(KSmtp::LoginJob::CramMD5);
        break;
    case TransportBase::EnumAuthenticationType::XOAUTH2:
295
        login->setPreferedAuthMode(KSmtp::LoginJob::XOAuth2);
296
297
298
299
300
301
302
303
304
305
306
        break;
    case TransportBase::EnumAuthenticationType::DIGEST_MD5:
        login->setPreferedAuthMode(KSmtp::LoginJob::DigestMD5);
        break;
    case TransportBase::EnumAuthenticationType::NTLM:
        login->setPreferedAuthMode(KSmtp::LoginJob::NTLM);
        break;
    case TransportBase::EnumAuthenticationType::GSSAPI:
        login->setPreferedAuthMode(KSmtp::LoginJob::GSSAPI);
        break;
    default:
307
        qCWarning(MAILTRANSPORT_SMTP_LOG) << "Unknown authentication mode" << q->transport()->authenticationTypeString();
308
        break;
Laurent Montel's avatar
Laurent Montel committed
309
310
    }

311
312
    q->connect(login, &KJob::result, q, &SmtpJob::slotResult);
    q->addSubjob(login);
313
314
315
    login->start();
    qCDebug(MAILTRANSPORT_SMTP_LOG) << "Login started";
}
Volker Krause's avatar
Volker Krause committed
316

317
318
319
320
321
322
323
324
void SmtpJob::startSendJob()
{
    auto send = new KSmtp::SendJob(d->session);
    send->setFrom(sender());
    send->setTo(to());
    send->setCc(cc());
    send->setBcc(bcc());
    send->setData(data());
Laurent Montel's avatar
Laurent Montel committed
325
    send->setDeliveryStatusNotification(deliveryStatusNotification());
326
327
328
329
330

    addSubjob(send);
    send->start();

    qCDebug(MAILTRANSPORT_SMTP_LOG) << "Send started";
331
332
}

Volker Krause's avatar
Volker Krause committed
333
334
bool SmtpJob::doKill()
{
335
    if (s_sessionPool.isDestroyed()) {
Laurent Montel's avatar
Laurent Montel committed
336
337
338
339
340
341
342
343
344
345
        return false;
    }

    if (!hasSubjobs()) {
        return true;
    }
    if (d->currentState == SmtpJobPrivate::Precommand) {
        return subjobs().first()->kill();
    } else if (d->currentState == SmtpJobPrivate::Smtp) {
        clearSubjobs();
346
        s_sessionPool->removeSession(d->session);
Laurent Montel's avatar
Laurent Montel committed
347
348
        return true;
    }
349
    return false;
Volker Krause's avatar
Volker Krause committed
350
351
}

Laurent Montel's avatar
Laurent Montel committed
352
void SmtpJob::slotResult(KJob *job)
353
{
354
    if (s_sessionPool.isDestroyed()) {
Laurent Montel's avatar
Laurent Montel committed
355
356
357
        return;
    }

Laurent Montel's avatar
Laurent Montel committed
358
    if (qobject_cast<KSmtp::LoginJob *>(job)) {
359
360
361
362
363
364
        if (job->error() == KSmtp::LoginJob::TokenExpired) {
            startPasswordRetrieval(/*force refresh */ true);
            return;
        }
    }

Laurent Montel's avatar
Laurent Montel committed
365
366
367
368
369
370
371
372
373
374
375
376
377
    // The job has finished, so we don't care about any further errors. Set
    // d->finished to true, so slaveError() knows about this and doesn't call
    // emitResult() anymore.
    // Sometimes, the SMTP slave emits more than one error
    //
    // The first error causes slotResult() to be called, but not slaveError(), since
    // the scheduler doesn't emit errors for connected slaves.
    //
    // The second error then causes slaveError() to be called (as the slave is no
    // longer connected), which does emitResult() a second time, which is invalid
    // (and triggers an assert in KMail).
    d->finished = true;

Yuri Chornoivan's avatar
Yuri Chornoivan committed
378
    // Normally, calling TransportJob::slotResult() would set the proper error code
Laurent Montel's avatar
Laurent Montel committed
379
380
381
382
383
384
385
386
387
388
389
390
391
    // for error() via KComposite::slotResult(). However, we can't call that here,
    // since that also emits the result signal.
    // In KMail, when there are multiple mails in the outbox, KMail tries to send
    // the next mail when it gets the result signal, which then would reuse the
    // old broken slave from the slave pool if there was an error.
    // To prevent that, we call TransportJob::slotResult() only after removing the
    // slave from the pool and calculate the error code ourselves.
    int errorCode = error();
    if (!errorCode) {
        errorCode = job->error();
    }

    if (errorCode && d->currentState == SmtpJobPrivate::Smtp) {
392
        s_sessionPool->removeSession(d->session);
Laurent Montel's avatar
Laurent Montel committed
393
394
395
396
397
398
399
400
401
402
        TransportJob::slotResult(job);
        return;
    }

    TransportJob::slotResult(job);
    if (!error() && d->currentState == SmtpJobPrivate::Precommand) {
        d->currentState = SmtpJobPrivate::Smtp;
        startSmtpJob();
        return;
    }
403
    if (!error() && !hasSubjobs()) {
Laurent Montel's avatar
Laurent Montel committed
404
405
        emitResult();
    }
406
407
}

408
#include "moc_smtpjob.cpp"