11#include "ksmtp_debug.h"
12#include "serverresponse_p.h"
15#include <KLocalizedString>
17#include <QJsonDocument>
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}};
38class LoginJobPrivate :
public JobPrivate
41 LoginJobPrivate(LoginJob *job, Session *session,
const QString &name)
42 : JobPrivate(session,
name)
47 ~LoginJobPrivate()
override =
default;
49 [[nodiscard]]
bool sasl_interact();
50 [[nodiscard]]
bool sasl_init();
51 [[nodiscard]]
bool sasl_challenge(
const QByteArray &data);
53 [[nodiscard]]
bool authenticate();
54 [[nodiscard]]
bool selectAuthentication();
56 [[nodiscard]] LoginJob::AuthMode authModeFromCommand(
const QByteArray &mech)
const;
57 [[nodiscard]] QByteArray authCommand(LoginJob::AuthMode mode)
const;
61 LoginJob::AuthMode m_preferedAuthMode{LoginJob::Login};
62 LoginJob::AuthMode m_actualAuthMode{LoginJob::UnknownAuth};
64 sasl_conn_t *m_saslConn =
nullptr;
65 sasl_interact_t *m_saslClient =
nullptr;
74LoginJob::LoginJob(
Session *session)
75 :
Job(*new LoginJobPrivate(this, session,
i18n(
"Login")))
79LoginJob::~LoginJob() =
default;
81void LoginJob::setUserName(
const QString &userName)
84 d->m_userName = userName;
87void LoginJob::setPassword(
const QString &password)
90 d->m_password = password;
93void LoginJob::setPreferedAuthMode(AuthMode mode)
97 if (mode == UnknownAuth) {
98 qCWarning(KSMTP_LOG) <<
"LoginJob: Cannot set preferred authentication mode to Unknown";
101 d->m_preferedAuthMode = mode;
104LoginJob::AuthMode LoginJob::usedAuthMode()
const
106 return d_func()->m_actualAuthMode;
109void LoginJob::doStart()
113 qFatal(
"LoginJob started despite session not being encrypted!");
116 if (!d->authenticate()) {
121void LoginJob::handleResponse(
const ServerResponse &r)
130 if (d->m_actualAuthMode == Plain) {
131 const QByteArray challengeResponse =
'\0' + d->m_userName.toUtf8() +
'\0' + d->m_password.toUtf8();
132 sendCommand(challengeResponse.
toBase64());
148bool LoginJobPrivate::selectAuthentication()
150 const QStringList availableModes = m_session->availableAuthModes();
153 m_actualAuthMode = m_preferedAuthMode;
155 m_actualAuthMode = LoginJob::Login;
157 m_actualAuthMode = LoginJob::Plain;
159 qCWarning(KSMTP_LOG) <<
"LoginJob: Couldn't choose an authentication method. Please retry with : " << availableModes;
160 q->setError(KJob::UserDefinedError);
161 q->setErrorText(
i18n(
"Could not authenticate to the SMTP server because no matching authentication method has been found"));
168bool LoginJobPrivate::sasl_init()
170 if (sasl_client_init(
nullptr) != SASL_OK) {
171 qCWarning(KSMTP_LOG) <<
"Failed to initialize SASL";
177bool LoginJobPrivate::sasl_interact()
179 sasl_interact_t *interact = m_saslClient;
181 while (interact->id != SASL_CB_LIST_END) {
182 qCDebug(KSMTP_LOG) <<
"SASL_INTERACT Id" << interact->id;
183 switch (interact->id) {
184 case SASL_CB_AUTHNAME: {
186 qCDebug(KSMTP_LOG) <<
"SASL_CB_[USER|AUTHNAME]: '" << m_userName <<
"'";
187 const auto username = m_userName.toUtf8();
188 interact->result = strdup(username.constData());
189 interact->len = username.size();
193 qCDebug(KSMTP_LOG) <<
"SASL_CB_PASS: [hidden]";
194 const auto pass = m_password.toUtf8();
195 interact->result = strdup(pass.constData());
196 interact->len = pass.size();
200 interact->result =
nullptr;
210bool LoginJobPrivate::sasl_challenge(
const QByteArray &challenge)
213 const char *out =
nullptr;
216 if (m_actualAuthMode == LoginJob::XOAuth2) {
219 const auto obj = doc.
object();
220 if (obj.value(QLatin1StringView(
"status")).
toString() == QLatin1StringView(
"400")) {
221 q->setError(LoginJob::TokenExpired);
222 q->setErrorText(
i18n(
"Token expired"));
232 result = sasl_client_step(m_saslConn, challenge.
isEmpty() ?
nullptr : challenge.
constData(), challenge.
size(), &m_saslClient, &out, &outLen);
233 if (result == SASL_INTERACT) {
234 if (!sasl_interact()) {
235 q->setError(LoginJob::UserDefinedError);
236 sasl_dispose(&m_saslConn);
244 if (result != SASL_OK && result != SASL_CONTINUE) {
246 qCWarning(KSMTP_LOG) <<
"sasl_client_step failed: " << result << saslError;
247 q->setError(LoginJob::UserDefinedError);
248 q->setErrorText(saslError);
249 sasl_dispose(&m_saslConn);
258bool LoginJobPrivate::authenticate()
260 if (!selectAuthentication()) {
265 q->setError(LoginJob::UserDefinedError);
266 q->setErrorText(
i18n(
"Login failed, cannot initialize the SASL library"));
270 int result = sasl_client_new(
"smtp", m_session->hostName().toUtf8().constData(),
nullptr,
nullptr, callbacks, 0, &m_saslConn);
271 if (result != SASL_OK) {
273 q->setError(LoginJob::UserDefinedError);
274 q->setErrorText(saslError);
279 const char *out =
nullptr;
280 const char *actualMech =
nullptr;
281 const auto authMode = authCommand(m_actualAuthMode);
284 qCDebug(KSMTP_LOG) <<
"Trying authmod" << authMode;
285 result = sasl_client_start(m_saslConn, authMode.constData(), &m_saslClient, &out, &outLen, &actualMech);
286 if (result == SASL_INTERACT) {
287 if (!sasl_interact()) {
288 sasl_dispose(&m_saslConn);
289 q->setError(LoginJob::UserDefinedError);
297 m_actualAuthMode = authModeFromCommand(actualMech);
299 if (result != SASL_CONTINUE && result != SASL_OK) {
301 qCWarning(KSMTP_LOG) <<
"sasl_client_start failed with:" << result << saslError;
302 q->setError(LoginJob::UserDefinedError);
303 q->setErrorText(saslError);
304 sasl_dispose(&m_saslConn);
309 q->sendCommand(
"AUTH " + authMode);
317LoginJob::AuthMode LoginJobPrivate::authModeFromCommand(
const QByteArray &mech)
const
319 if (qstrnicmp(mech.
constData(),
"PLAIN", 5) == 0) {
320 return LoginJob::Plain;
321 }
else if (qstrnicmp(mech.
constData(),
"LOGIN", 5) == 0) {
322 return LoginJob::Login;
323 }
else if (qstrnicmp(mech.
constData(),
"CRAM-MD5", 8) == 0) {
324 return LoginJob::CramMD5;
325 }
else if (qstrnicmp(mech.
constData(),
"DIGEST-MD5", 10) == 0) {
326 return LoginJob::DigestMD5;
327 }
else if (qstrnicmp(mech.
constData(),
"GSSAPI", 6) == 0) {
328 return LoginJob::GSSAPI;
329 }
else if (qstrnicmp(mech.
constData(),
"NTLM", 4) == 0) {
330 return LoginJob::NTLM;
331 }
else if (qstrnicmp(mech.
constData(),
"ANONYMOUS", 9) == 0) {
332 return LoginJob::Anonymous;
333 }
else if (qstrnicmp(mech.
constData(),
"XOAUTH2", 7) == 0) {
334 return LoginJob::XOAuth2;
336 return LoginJob::UnknownAuth;
340QByteArray LoginJobPrivate::authCommand(LoginJob::AuthMode mode)
const
343 case LoginJob::Plain:
344 return QByteArrayLiteral(
"PLAIN");
345 case LoginJob::Login:
346 return QByteArrayLiteral(
"LOGIN");
347 case LoginJob::CramMD5:
348 return QByteArrayLiteral(
"CRAM-MD5");
349 case LoginJob::DigestMD5:
350 return QByteArrayLiteral(
"DIGEST-MD5");
351 case LoginJob::GSSAPI:
352 return QByteArrayLiteral(
"GSSAPI");
354 return QByteArrayLiteral(
"NTLM");
355 case LoginJob::Anonymous:
356 return QByteArrayLiteral(
"ANONYMOUS");
357 case LoginJob::XOAuth2:
358 return QByteArrayLiteral(
"XOAUTH2");
359 case LoginJob::UnknownAuth:
365#include "moc_loginjob.cpp"
@ Authenticated
The Session is ready to send email.
@ Unencrypted
Use no encryption.
QString i18n(const char *text, const TYPE &arg...)
char * toString(const EngineQuery &query)
QString name(StandardAction id)
const char * constData() const const
QByteArray fromBase64(const QByteArray &base64, Base64Options options)
QByteArray fromRawData(const char *data, qsizetype size)
bool isEmpty() const const
qsizetype size() 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
QString fromLatin1(QByteArrayView str)
QString fromUtf8(QByteArrayView str)
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const