10 #include <KLocalizedString>
12 #include "kimap_debug.h"
14 #include "capabilitiesjob.h"
16 #include "response_p.h"
18 #include "session_p.h"
23 #include <sasl/sasl.h>
26 static 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}};
37 class LoginJobPrivate :
public JobPrivate
40 enum AuthState { PreStartTlsCapability = 0, StartTls,
Capability, Login, Authenticate };
42 LoginJobPrivate(LoginJob *job, Session *session,
const QString &name)
43 : JobPrivate(session,
name)
45 , encryptionMode(LoginJob::Unencrypted)
47 , plainLoginDisabled(false)
50 client_interact =
nullptr;
57 bool startAuthentication();
59 void sslResponse(
bool response);
60 void saveServerGreeting(
const Response &response);
69 LoginJob::EncryptionMode encryptionMode;
73 bool plainLoginDisabled;
76 sasl_interact_t *client_interact;
80 using namespace KIMAP;
82 bool LoginJobPrivate::sasl_interact()
84 qCDebug(KIMAP_LOG) <<
"sasl_interact";
85 sasl_interact_t *interact = client_interact;
89 for (; interact->id != SASL_CB_LIST_END; interact++) {
90 if (interact->id == SASL_CB_AUTHNAME || interact->id == SASL_CB_PASS) {
96 interact = client_interact;
97 while (interact->id != SASL_CB_LIST_END) {
98 qCDebug(KIMAP_LOG) <<
"SASL_INTERACT id:" << interact->id;
99 switch (interact->id) {
100 case SASL_CB_AUTHNAME:
101 if (!authorizationName.isEmpty()) {
102 qCDebug(KIMAP_LOG) <<
"SASL_CB_[AUTHNAME]: '" << authorizationName <<
"'";
103 interact->result = strdup(authorizationName.toUtf8().constData());
104 interact->len = strlen((
const char *)interact->result);
109 qCDebug(KIMAP_LOG) <<
"SASL_CB_[USER|AUTHNAME]: '" << userName <<
"'";
110 interact->result = strdup(userName.toUtf8().constData());
111 interact->len = strlen((
const char *)interact->result);
114 qCDebug(KIMAP_LOG) <<
"SASL_CB_PASS: [hidden]";
115 interact->result = strdup(password.toUtf8().constData());
116 interact->len = strlen((
const char *)interact->result);
119 interact->result =
nullptr;
128 LoginJob::LoginJob(Session *session)
129 : Job(*new LoginJobPrivate(this, session,
i18n(
"Login")))
132 qCDebug(KIMAP_LOG) <<
this;
135 LoginJob::~LoginJob()
137 qCDebug(KIMAP_LOG) <<
this;
140 QString LoginJob::userName()
const
146 void LoginJob::setUserName(
const QString &userName)
149 d->userName = userName;
152 QString LoginJob::authorizationName()
const
155 return d->authorizationName;
158 void LoginJob::setAuthorizationName(
const QString &authorizationName)
161 d->authorizationName = authorizationName;
164 QString LoginJob::password()
const
170 void LoginJob::setPassword(
const QString &password)
173 d->password = password;
176 void LoginJob::doStart()
180 qCDebug(KIMAP_LOG) <<
this;
182 if (session()->state() == Session::Authenticated || session()->state() == Session::Selected) {
183 setError(UserDefinedError);
184 setErrorText(
i18n(
"IMAP session in the wrong state for authentication"));
190 connect(d->sessionInternal(), &KIMAP::SessionPrivate::encryptionNegotiationResult,
this, [d](
bool result) {
191 d->sslResponse(result);
195 EncryptionMode encryptionMode = d->encryptionMode;
197 const auto negotiatedEncryption = d->sessionInternal()->negotiatedEncryption();
200 d->sslResponse(
true);
204 if (encryptionMode == SSLorTLS) {
206 }
else if (encryptionMode == STARTTLS) {
208 d->authState = LoginJobPrivate::PreStartTlsCapability;
209 d->tags << d->sessionInternal()->sendCommand(
"CAPABILITY");
210 }
else if (encryptionMode == Unencrypted) {
211 if (d->authMode.isEmpty()) {
212 d->authState = LoginJobPrivate::Login;
213 qCDebug(KIMAP_LOG) <<
"sending LOGIN";
214 d->tags << d->sessionInternal()->sendCommand(
"LOGIN",
215 '"' +
quoteIMAP(d->userName).toUtf8() +
'"' +
' ' +
'"' +
quoteIMAP(d->password).toUtf8() +
'"');
217 if (!d->startAuthentication()) {
224 void LoginJob::handleResponse(
const Response &response)
228 if (response.content.isEmpty()) {
234 if (d->authState == LoginJobPrivate::Capability) {
235 commandName =
i18n(
"Capability");
236 }
else if (d->authState == LoginJobPrivate::StartTls) {
237 commandName =
i18n(
"StartTls");
240 enum ResponseCode {
OK, ERR, UNTAGGED, CONTINUATION, MALFORMED };
242 QByteArray tag = response.content.first().toString();
243 ResponseCode code =
OK;
245 qCDebug(KIMAP_LOG) << commandName << tag;
249 }
else if (tag ==
"*") {
250 if (response.content.size() < 2) {
255 }
else if (d->tags.contains(tag)) {
256 if (response.content.size() < 2) {
258 }
else if (response.content[1].toString() ==
"OK") {
272 if (d->authState == LoginJobPrivate::Authenticate) {
273 sasl_dispose(&d->conn);
276 setError(UserDefinedError);
277 setErrorText(
i18n(
"%1 failed, server replied: %2", commandName,
QLatin1String(response.toString().constData())));
283 if (response.content[1].toString() ==
"CAPABILITY") {
284 d->capabilities.clear();
286 while (p != response.content.end()) {
288 d->capabilities << capability;
290 d->plainLoginDisabled =
true;
294 qCDebug(KIMAP_LOG) <<
"Capabilities updated: " << d->capabilities;
299 if (d->authState != LoginJobPrivate::Authenticate) {
307 if (response.content.size() > 1 && response.content.at(1).toString() ==
"OK") {
311 if (!d->authorizationName.isEmpty()) {
312 challengeResponse += d->authorizationName.toUtf8();
314 challengeResponse +=
'\0';
315 challengeResponse += d->userName.toUtf8();
316 challengeResponse +=
'\0';
317 challengeResponse += d->password.toUtf8();
318 challengeResponse = challengeResponse.
toBase64();
319 d->sessionInternal()->sendData(challengeResponse);
320 }
else if (response.content.size() >= 2) {
332 switch (d->authState) {
333 case LoginJobPrivate::PreStartTlsCapability:
335 d->authState = LoginJobPrivate::StartTls;
336 d->tags << d->sessionInternal()->sendCommand(
"STARTTLS");
338 qCWarning(KIMAP_LOG) <<
"STARTTLS not supported by server!";
339 setError(UserDefinedError);
340 setErrorText(
i18n(
"STARTTLS is not supported by the server, try using SSL/TLS instead."));
345 case LoginJobPrivate::StartTls:
349 case LoginJobPrivate::Capability:
351 if (d->encryptionMode != Unencrypted && d->sessionInternal()->negotiatedEncryption() ==
QSsl::UnknownProtocol) {
352 setError(LoginJob::UserDefinedError);
353 setErrorText(
i18n(
"Internal error, tried to login before encryption"));
359 if (d->authMode.isEmpty()) {
360 if (d->plainLoginDisabled) {
361 setError(UserDefinedError);
362 setErrorText(
i18n(
"Login failed, plain login is disabled by the server."));
365 d->authState = LoginJobPrivate::Login;
366 d->tags << d->sessionInternal()->sendCommand(
"LOGIN",
367 '"' +
quoteIMAP(d->userName).toUtf8() +
'"' +
' ' +
'"' +
quoteIMAP(d->password).toUtf8()
371 bool authModeSupported =
false;
373 for (
const QString &capability : std::as_const(d->capabilities)) {
375 if (
QStringView(capability).mid(5) == d->authMode) {
376 authModeSupported =
true;
381 if (!authModeSupported) {
382 setError(UserDefinedError);
383 setErrorText(
i18n(
"Login failed, authentication mode %1 is not supported by the server.", d->authMode));
385 }
else if (!d->startAuthentication()) {
391 case LoginJobPrivate::Authenticate:
392 sasl_dispose(&d->conn);
395 case LoginJobPrivate::Login:
396 d->saveServerGreeting(response);
402 if (code == MALFORMED) {
403 setErrorText(
i18n(
"%1 failed, malformed reply from the server.", commandName));
408 bool LoginJobPrivate::startAuthentication()
412 q->setError(LoginJob::UserDefinedError);
413 q->setErrorText(
i18n(
"Login failed, client cannot initialize the SASL library."));
417 authState = LoginJobPrivate::Authenticate;
418 const char *out =
nullptr;
420 const char *mechusing =
nullptr;
422 int result = sasl_client_new(
"imap", m_session->hostName().toLatin1().constData(),
nullptr,
nullptr, callbacks, 0, &conn);
423 if (result != SASL_OK) {
425 qCWarning(KIMAP_LOG) <<
"sasl_client_new failed with:" << result << saslError;
426 q->setError(LoginJob::UserDefinedError);
427 q->setErrorText(saslError);
432 qCDebug(KIMAP_LOG) <<
"Trying authmod" << authMode.
toLatin1();
433 result = sasl_client_start(conn,
434 authMode.toLatin1().constData(),
440 if (result == SASL_INTERACT) {
441 if (!sasl_interact()) {
443 q->setError(LoginJob::UserDefinedError);
447 }
while (result == SASL_INTERACT);
449 if (result != SASL_CONTINUE && result != SASL_OK) {
451 qCWarning(KIMAP_LOG) <<
"sasl_client_start failed with:" << result << saslError;
452 q->setError(LoginJob::UserDefinedError);
453 q->setErrorText(saslError);
462 tags << sessionInternal()->sendCommand(
"AUTHENTICATE", authMode.toLatin1());
464 tags << sessionInternal()->sendCommand(
"AUTHENTICATE", authMode.toLatin1() +
' ' + challenge);
470 bool LoginJobPrivate::answerChallenge(
const QByteArray &data)
474 const char *out =
nullptr;
477 result = sasl_client_step(conn, challenge.
isEmpty() ?
nullptr : challenge.
data(), challenge.
size(), &client_interact, &out, &outlen);
479 if (result == SASL_INTERACT) {
480 if (!sasl_interact()) {
481 q->setError(LoginJob::UserDefinedError);
486 }
while (result == SASL_INTERACT);
488 if (result != SASL_CONTINUE && result != SASL_OK) {
490 qCWarning(KIMAP_LOG) <<
"sasl_client_step failed with:" << result << saslError;
491 q->setError(LoginJob::UserDefinedError);
492 q->setErrorText(saslError);
500 sessionInternal()->sendData(challenge);
505 void LoginJobPrivate::sslResponse(
bool response)
508 authState = LoginJobPrivate::Capability;
509 tags << sessionInternal()->sendCommand(
"CAPABILITY");
511 q->setError(LoginJob::UserDefinedError);
512 q->setErrorText(
i18n(
"Login failed, TLS negotiation failed."));
513 encryptionMode = LoginJob::Unencrypted;
518 void LoginJob::setEncryptionMode(EncryptionMode mode)
521 d->encryptionMode = mode;
524 LoginJob::EncryptionMode LoginJob::encryptionMode()
527 return d->encryptionMode;
530 void LoginJob::setAuthenticationMode(AuthenticationMode mode)
538 d->authMode = QStringLiteral(
"LOGIN");
541 d->authMode = QStringLiteral(
"PLAIN");
544 d->authMode = QStringLiteral(
"CRAM-MD5");
547 d->authMode = QStringLiteral(
"DIGEST-MD5");
550 d->authMode = QStringLiteral(
"GSSAPI");
553 d->authMode = QStringLiteral(
"ANONYMOUS");
556 d->authMode = QStringLiteral(
"XOAUTH2");
563 void LoginJob::connectionLost()
567 qCWarning(KIMAP_LOG) <<
"Connection to server lost " << d->m_socketError;
569 setError(KJob::UserDefinedError);
570 setErrorText(
i18n(
"SSL handshake failed."));
573 setError(ERR_COULD_NOT_CONNECT);
574 setErrorText(
i18n(
"Connection to server lost."));
579 void LoginJobPrivate::saveServerGreeting(
const Response &response)
584 for (
int i = 2; i < response.content.size(); i++) {
585 if (response.content.at(i).type() == Response::Part::List) {
591 serverGreeting.chop(1);
592 serverGreeting += QStringLiteral(
") ");
597 serverGreeting.chop(1);
600 QString LoginJob::serverGreeting()
const
603 return d->serverGreeting;
606 #include "moc_loginjob.cpp"