KSMTP

loginjob.cpp
1/*
2 SPDX-FileCopyrightText: 2010 BetterInbox <contact@betterinbox.com>
3 SPDX-FileContributor: Christophe Laveault <christophe@betterinbox.com>
4 SPDX-FileContributor: Gregory Schlomoff <gregory.schlomoff@gmail.com>
5
6 SPDX-License-Identifier: LGPL-2.1-or-later
7*/
8
9#include "loginjob.h"
10#include "job_p.h"
11#include "ksmtp_debug.h"
12#include "serverresponse_p.h"
13#include "session_p.h"
14
15#include <KLocalizedString>
16
17#include <QJsonDocument>
18#include <QJsonObject>
19
20extern "C" {
21#include <sasl/sasl.h>
22}
23
24namespace
25{
26static const sasl_callback_t callbacks[] = {{SASL_CB_ECHOPROMPT, nullptr, nullptr},
27 {SASL_CB_NOECHOPROMPT, nullptr, nullptr},
28 {SASL_CB_GETREALM, nullptr, nullptr},
29 {SASL_CB_USER, nullptr, nullptr},
30 {SASL_CB_AUTHNAME, nullptr, nullptr},
31 {SASL_CB_PASS, nullptr, nullptr},
32 {SASL_CB_CANON_USER, nullptr, nullptr},
33 {SASL_CB_LIST_END, nullptr, nullptr}};
34}
35
36namespace KSmtp
37{
38class LoginJobPrivate : public JobPrivate
39{
40public:
41 LoginJobPrivate(LoginJob *job, Session *session, const QString &name)
42 : JobPrivate(session, name)
43 , m_preferedAuthMode(LoginJob::Login)
44 , m_actualAuthMode(LoginJob::UnknownAuth)
45 , q(job)
46 {
47 }
48
49 ~LoginJobPrivate() override = default;
50
51 [[nodiscard]] bool sasl_interact();
52 [[nodiscard]] bool sasl_init();
53 [[nodiscard]] bool sasl_challenge(const QByteArray &data);
54
55 [[nodiscard]] bool authenticate();
56 [[nodiscard]] bool selectAuthentication();
57
58 [[nodiscard]] LoginJob::AuthMode authModeFromCommand(const QByteArray &mech) const;
59 [[nodiscard]] QByteArray authCommand(LoginJob::AuthMode mode) const;
60
61 QString m_userName;
62 QString m_password;
63 LoginJob::AuthMode m_preferedAuthMode;
64 LoginJob::AuthMode m_actualAuthMode;
65
66 sasl_conn_t *m_saslConn = nullptr;
67 sasl_interact_t *m_saslClient = nullptr;
68
69private:
70 LoginJob *const q;
71};
72}
73
74using namespace KSmtp;
75
76LoginJob::LoginJob(Session *session)
77 : Job(*new LoginJobPrivate(this, session, i18n("Login")))
78{
79}
80
81LoginJob::~LoginJob() = default;
82
83void LoginJob::setUserName(const QString &userName)
84{
86 d->m_userName = userName;
87}
88
89void LoginJob::setPassword(const QString &password)
90{
92 d->m_password = password;
93}
94
95void LoginJob::setPreferedAuthMode(AuthMode mode)
96{
98
99 if (mode == UnknownAuth) {
100 qCWarning(KSMTP_LOG) << "LoginJob: Cannot set preferred authentication mode to Unknown";
101 return;
102 }
103 d->m_preferedAuthMode = mode;
104}
105
106LoginJob::AuthMode LoginJob::usedAuthMode() const
107{
108 return d_func()->m_actualAuthMode;
109}
110
111void LoginJob::doStart()
112{
113 Q_D(LoginJob);
114 if (d->sessionInternal()->negotiatedEncryption() == QSsl::UnknownProtocol && d->m_session->encryptionMode() != Session::Unencrypted) {
115 qFatal("LoginJob started despite session not being encrypted!");
116 }
117
118 if (!d->authenticate()) {
119 emitResult();
120 }
121}
122
123void LoginJob::handleResponse(const ServerResponse &r)
124{
125 Q_D(LoginJob);
126
127 // Handle server errors
128 handleErrors(r);
129
130 // Send account data
131 if (r.isCode(334)) {
132 if (d->m_actualAuthMode == Plain) {
133 const QByteArray challengeResponse = '\0' + d->m_userName.toUtf8() + '\0' + d->m_password.toUtf8();
134 sendCommand(challengeResponse.toBase64());
135 } else {
136 if (!d->sasl_challenge(QByteArray::fromBase64(r.text()))) {
137 emitResult();
138 }
139 }
140 return;
141 }
142
143 // Final agreement
144 if (r.isCode(235)) {
145 d->sessionInternal()->setState(Session::Authenticated);
146 emitResult();
147 }
148}
149
150bool LoginJobPrivate::selectAuthentication()
151{
152 const QStringList availableModes = m_session->availableAuthModes();
153
154 if (availableModes.contains(QString::fromLatin1(authCommand(m_preferedAuthMode)))) {
155 m_actualAuthMode = m_preferedAuthMode;
156 } else if (availableModes.contains(QString::fromLatin1(authCommand(LoginJob::Login)))) {
157 m_actualAuthMode = LoginJob::Login;
158 } else if (availableModes.contains(QString::fromLatin1(authCommand(LoginJob::Plain)))) {
159 m_actualAuthMode = LoginJob::Plain;
160 } else {
161 qCWarning(KSMTP_LOG) << "LoginJob: Couldn't choose an authentication method. Please retry with : " << availableModes;
162 q->setError(KJob::UserDefinedError);
163 q->setErrorText(i18n("Could not authenticate to the SMTP server because no matching authentication method has been found"));
164 return false;
165 }
166
167 return true;
168}
169
170bool LoginJobPrivate::sasl_init()
171{
172 if (sasl_client_init(nullptr) != SASL_OK) {
173 qCWarning(KSMTP_LOG) << "Failed to initialize SASL";
174 return false;
175 }
176 return true;
177}
178
179bool LoginJobPrivate::sasl_interact()
180{
181 sasl_interact_t *interact = m_saslClient;
182
183 while (interact->id != SASL_CB_LIST_END) {
184 qCDebug(KSMTP_LOG) << "SASL_INTERACT Id" << interact->id;
185 switch (interact->id) {
186 case SASL_CB_AUTHNAME: {
187 // case SASL_CB_USER:
188 qCDebug(KSMTP_LOG) << "SASL_CB_[USER|AUTHNAME]: '" << m_userName << "'";
189 const auto username = m_userName.toUtf8();
190 interact->result = strdup(username.constData());
191 interact->len = username.size();
192 break;
193 }
194 case SASL_CB_PASS: {
195 qCDebug(KSMTP_LOG) << "SASL_CB_PASS: [hidden]";
196 const auto pass = m_password.toUtf8();
197 interact->result = strdup(pass.constData());
198 interact->len = pass.size();
199 break;
200 }
201 default:
202 interact->result = nullptr;
203 interact->len = 0;
204 break;
205 }
206 ++interact;
207 }
208
209 return true;
210}
211
212bool LoginJobPrivate::sasl_challenge(const QByteArray &challenge)
213{
214 int result = -1;
215 const char *out = nullptr;
216 uint outLen = 0;
217
218 if (m_actualAuthMode == LoginJob::XOAuth2) {
219 QJsonDocument doc = QJsonDocument::fromJson(challenge);
220 if (!doc.isNull() && doc.isObject()) {
221 const auto obj = doc.object();
222 if (obj.value(QLatin1StringView("status")).toString() == QLatin1StringView("400")) {
223 q->setError(LoginJob::TokenExpired);
224 q->setErrorText(i18n("Token expired"));
225 // https://developers.google.com/gmail/imap/xoauth2-protocol#error_response_2
226 // "The client sends an empty response ("\r\n") to the challenge containing the error message."
227 q->sendCommand("");
228 return false;
229 }
230 }
231 }
232
233 for (;;) {
234 result = sasl_client_step(m_saslConn, challenge.isEmpty() ? nullptr : challenge.constData(), challenge.size(), &m_saslClient, &out, &outLen);
235 if (result == SASL_INTERACT) {
236 if (!sasl_interact()) {
237 q->setError(LoginJob::UserDefinedError);
238 sasl_dispose(&m_saslConn);
239 return false;
240 }
241 } else {
242 break;
243 }
244 }
245
246 if (result != SASL_OK && result != SASL_CONTINUE) {
247 const QString saslError = QString::fromUtf8(sasl_errdetail(m_saslConn));
248 qCWarning(KSMTP_LOG) << "sasl_client_step failed: " << result << saslError;
249 q->setError(LoginJob::UserDefinedError);
250 q->setErrorText(saslError);
251 sasl_dispose(&m_saslConn);
252 return false;
253 }
254
255 q->sendCommand(QByteArray::fromRawData(out, outLen).toBase64());
256
257 return true;
258}
259
260bool LoginJobPrivate::authenticate()
261{
262 if (!selectAuthentication()) {
263 return false;
264 }
265
266 if (!sasl_init()) {
267 q->setError(LoginJob::UserDefinedError);
268 q->setErrorText(i18n("Login failed, cannot initialize the SASL library"));
269 return false;
270 }
271
272 int result = sasl_client_new("smtp", m_session->hostName().toUtf8().constData(), nullptr, nullptr, callbacks, 0, &m_saslConn);
273 if (result != SASL_OK) {
274 const auto saslError = QString::fromUtf8(sasl_errdetail(m_saslConn));
275 q->setError(LoginJob::UserDefinedError);
276 q->setErrorText(saslError);
277 return false;
278 }
279
280 uint outLen = 0;
281 const char *out = nullptr;
282 const char *actualMech = nullptr;
283 const auto authMode = authCommand(m_actualAuthMode);
284
285 for (;;) {
286 qCDebug(KSMTP_LOG) << "Trying authmod" << authMode;
287 result = sasl_client_start(m_saslConn, authMode.constData(), &m_saslClient, &out, &outLen, &actualMech);
288 if (result == SASL_INTERACT) {
289 if (!sasl_interact()) {
290 sasl_dispose(&m_saslConn);
291 q->setError(LoginJob::UserDefinedError);
292 return false;
293 }
294 } else {
295 break;
296 }
297 }
298
299 m_actualAuthMode = authModeFromCommand(actualMech);
300
301 if (result != SASL_CONTINUE && result != SASL_OK) {
302 const auto saslError = QString::fromUtf8(sasl_errdetail(m_saslConn));
303 qCWarning(KSMTP_LOG) << "sasl_client_start failed with:" << result << saslError;
304 q->setError(LoginJob::UserDefinedError);
305 q->setErrorText(saslError);
306 sasl_dispose(&m_saslConn);
307 return false;
308 }
309
310 if (outLen == 0) {
311 q->sendCommand("AUTH " + authMode);
312 } else {
313 q->sendCommand("AUTH " + authMode + ' ' + QByteArray::fromRawData(out, outLen).toBase64());
314 }
315
316 return true;
317}
318
319LoginJob::AuthMode LoginJobPrivate::authModeFromCommand(const QByteArray &mech) const
320{
321 if (qstrnicmp(mech.constData(), "PLAIN", 5) == 0) {
322 return LoginJob::Plain;
323 } else if (qstrnicmp(mech.constData(), "LOGIN", 5) == 0) {
324 return LoginJob::Login;
325 } else if (qstrnicmp(mech.constData(), "CRAM-MD5", 8) == 0) {
326 return LoginJob::CramMD5;
327 } else if (qstrnicmp(mech.constData(), "DIGEST-MD5", 10) == 0) {
328 return LoginJob::DigestMD5;
329 } else if (qstrnicmp(mech.constData(), "GSSAPI", 6) == 0) {
330 return LoginJob::GSSAPI;
331 } else if (qstrnicmp(mech.constData(), "NTLM", 4) == 0) {
332 return LoginJob::NTLM;
333 } else if (qstrnicmp(mech.constData(), "ANONYMOUS", 9) == 0) {
334 return LoginJob::Anonymous;
335 } else if (qstrnicmp(mech.constData(), "XOAUTH2", 7) == 0) {
336 return LoginJob::XOAuth2;
337 } else {
338 return LoginJob::UnknownAuth;
339 }
340}
341
342QByteArray LoginJobPrivate::authCommand(LoginJob::AuthMode mode) const
343{
344 switch (mode) {
345 case LoginJob::Plain:
346 return QByteArrayLiteral("PLAIN");
347 case LoginJob::Login:
348 return QByteArrayLiteral("LOGIN");
349 case LoginJob::CramMD5:
350 return QByteArrayLiteral("CRAM-MD5");
351 case LoginJob::DigestMD5:
352 return QByteArrayLiteral("DIGEST-MD5");
353 case LoginJob::GSSAPI:
354 return QByteArrayLiteral("GSSAPI");
355 case LoginJob::NTLM:
356 return QByteArrayLiteral("NTLM");
357 case LoginJob::Anonymous:
358 return QByteArrayLiteral("ANONYMOUS");
359 case LoginJob::XOAuth2:
360 return QByteArrayLiteral("XOAUTH2");
361 case LoginJob::UnknownAuth:
362 return ""; // Should not happen
363 }
364 return {};
365}
366
367#include "moc_loginjob.cpp"
void setErrorText(const QString &errorText)
void emitResult()
void setError(int errorCode)
The Job class.
Definition job.h:25
The LoginJob class.
Definition loginjob.h:22
@ Authenticated
The Session is ready to send email.
Definition session.h:33
@ Unencrypted
Use no encryption.
Definition session.h:40
QString i18n(const char *text, const TYPE &arg...)
char * toString(const EngineQuery &query)
QString name(GameStandardAction id)
const char * constData() const const
QByteArray fromBase64(const QByteArray &base64, Base64Options options)
QByteArray fromRawData(const char *data, qsizetype size)
bool isEmpty() const const
QByteArray toBase64(Base64Options options) const const
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
bool isNull() const const
bool isObject() const const
QJsonObject object() const const
UnknownProtocol
QString fromLatin1(QByteArrayView str)
QString fromUtf8(QByteArrayView str)
QByteArray toUtf8() const const
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
Q_D(Todo)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Jul 26 2024 11:58:42 by doxygen 1.11.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.