24 #include "kimap_debug.h"
27 #include "message_p.h"
28 #include "session_p.h"
34 #include <sasl/sasl.h>
37 static const sasl_callback_t callbacks[] = {
38 { SASL_CB_ECHOPROMPT, Q_NULLPTR,
nullptr },
39 { SASL_CB_NOECHOPROMPT, Q_NULLPTR,
nullptr },
40 { SASL_CB_GETREALM, Q_NULLPTR,
nullptr },
41 { SASL_CB_USER, Q_NULLPTR,
nullptr },
42 { SASL_CB_AUTHNAME, Q_NULLPTR,
nullptr },
43 { SASL_CB_PASS, Q_NULLPTR,
nullptr },
44 { SASL_CB_CANON_USER, Q_NULLPTR,
nullptr },
45 { SASL_CB_LIST_END, Q_NULLPTR,
nullptr }
50 class LoginJobPrivate :
public JobPrivate
60 LoginJobPrivate(LoginJob *job, Session *session,
const QString &name) : JobPrivate(session,
name), q(job)
63 client_interact = Q_NULLPTR;
65 ~LoginJobPrivate() { }
68 bool startAuthentication();
69 void sendPlainLogin();
71 void sslResponse(
bool response);
72 void saveServerGreeting(
const Message &response);
74 void retrieveCapabilities();
84 bool startTls =
false;
86 AuthState authState = Login;
88 bool plainLoginDisabled =
false;
89 bool connectionIsEncrypted =
false;
92 sasl_interact_t *client_interact;
96 using namespace KIMAP2;
98 bool LoginJobPrivate::sasl_interact()
100 qCDebug(KIMAP2_LOG) <<
"sasl_interact";
101 sasl_interact_t *interact = client_interact;
105 for (; interact->id != SASL_CB_LIST_END; interact++) {
106 if (interact->id == SASL_CB_AUTHNAME ||
107 interact->id == SASL_CB_PASS) {
113 interact = client_interact;
114 while (interact->id != SASL_CB_LIST_END) {
115 qCDebug(KIMAP2_LOG) <<
"SASL_INTERACT id:" << interact->id;
116 switch (interact->id) {
117 case SASL_CB_AUTHNAME:
118 if (!authorizationName.isEmpty()) {
119 qCDebug(KIMAP2_LOG) <<
"SASL_CB_[AUTHNAME]: '" << authorizationName <<
"'";
120 interact->result = strdup(authorizationName.toUtf8());
121 interact->len = strlen((
const char *) interact->result);
125 qCDebug(KIMAP2_LOG) <<
"SASL_CB_[USER|AUTHNAME]: '" << userName <<
"'";
126 interact->result = strdup(userName.toUtf8());
127 interact->len = strlen((
const char *) interact->result);
130 qCDebug(KIMAP2_LOG) <<
"SASL_CB_PASS: [hidden]";
131 interact->result = strdup(password.toUtf8());
132 interact->len = strlen((
const char *) interact->result);
135 interact->result = Q_NULLPTR;
145 LoginJob::LoginJob(Session *session)
146 : Job(*new LoginJobPrivate(this, session,
QString::fromUtf8(
"Login")))
148 qCDebug(KIMAP2_LOG) <<
this;
151 LoginJob::~LoginJob()
153 qCDebug(KIMAP2_LOG) <<
this;
156 QString LoginJob::userName()
const
162 void LoginJob::setUserName(
const QString &userName)
165 d->userName = userName;
168 QString LoginJob::authorizationName()
const
171 return d->authorizationName;
174 void LoginJob::setAuthorizationName(
const QString &authorizationName)
177 d->authorizationName = authorizationName;
180 QString LoginJob::password()
const
186 void LoginJob::setPassword(
const QString &password)
189 d->password = password;
218 void LoginJob::doStart()
222 qCDebug(KIMAP2_LOG) <<
"doStart" <<
this;
224 connect(d->sessionInternal(), SIGNAL(encryptionNegotiationResult(
bool)),
this, SLOT(sslResponse(
bool)));
226 if (session()->state() == Session::Disconnected) {
227 auto guard =
new QObject(
this);
228 QObject::connect(session(), &Session::stateChanged, guard, [d, guard](KIMAP2::Session::State newState, KIMAP2::Session::State) {
229 qCDebug(KIMAP2_LOG) <<
"Session state changed" << newState;
235 d->sessionInternal()->startSsl(d->encryptionMode);
240 qCInfo(KIMAP2_LOG) <<
"Session is ready, carring on";
247 void LoginJobPrivate::login()
250 if (q->session()->isConnected()) {
251 q->setError(LoginJob::UserDefinedError);
252 q->setErrorText(
QString::fromUtf8(
"IMAP session in the wrong state for authentication"));
259 qCInfo(KIMAP2_LOG) <<
"Starting with tls";
260 authState = LoginJobPrivate::StartTls;
266 retrieveCapabilities();
268 qCInfo(KIMAP2_LOG) <<
"Waiting for encryption before retrieveing capabilities.";
274 void LoginJobPrivate::sslResponse(
bool response)
276 qCDebug(KIMAP2_LOG) <<
"Got an ssl response " << response;
277 connectionIsEncrypted = response;
282 if (m_session->state() != Session::Disconnected) {
283 retrieveCapabilities();
286 q->setError(LoginFailed);
293 void LoginJobPrivate::retrieveCapabilities()
295 qCDebug(KIMAP2_LOG) <<
"Retrieving capabilities.";
296 authState = LoginJobPrivate::Capability;
300 void LoginJob::handleResponse(
const Message &response)
304 if (response.content.isEmpty()) {
309 QString commandName = QStringLiteral(
"Login");
310 if (d->authState == LoginJobPrivate::Capability) {
311 commandName = QStringLiteral(
"Capability");
312 }
else if (d->authState == LoginJobPrivate::StartTls) {
313 commandName = QStringLiteral(
"StartTls");
324 QByteArray tag = response.content.first().toString();
325 ResponseCode code =
OK;
327 qCDebug(KIMAP2_LOG) << commandName << tag;
331 }
else if (tag ==
"*") {
332 if (response.content.size() < 2) {
337 }
else if (d->tags.contains(tag)) {
338 if (response.content.size() < 2) {
340 }
else if (response.content[1].toString() ==
"OK") {
354 if (d->authState == LoginJobPrivate::Authenticate) {
355 sasl_dispose(&d->conn);
358 setError(LoginFailed);
359 setErrorText(
QString(
"%1 failed, server replied: %2").arg(commandName).arg(
QLatin1String(response.toString().constData())));
365 if (response.content[1].toString() ==
"CAPABILITY") {
367 while (p != response.content.end()) {
369 d->capabilities << capability;
371 d->plainLoginDisabled =
true;
375 qCInfo(KIMAP2_LOG) <<
"Capabilities updated: " << d->capabilities;
380 if (d->authState != LoginJobPrivate::Authenticate) {
388 if (response.content.size() > 1 && response.content.at(1).toString() ==
"OK") {
392 if (!d->authorizationName.isEmpty()) {
393 challengeResponse += d->authorizationName.toUtf8();
395 challengeResponse +=
'\0';
396 challengeResponse += d->userName.toUtf8();
397 challengeResponse +=
'\0';
398 challengeResponse += d->password.toUtf8();
399 challengeResponse = challengeResponse.
toBase64();
400 d->sessionInternal()->sendData(challengeResponse);
401 }
else if (response.content.size() >= 2) {
412 switch (d->authState) {
413 case LoginJobPrivate::StartTls:
415 d->sessionInternal()->startSsl(d->encryptionMode);
417 case LoginJobPrivate::Capability:
419 if (d->authMode.isEmpty()) {
420 if (d->plainLoginDisabled) {
421 setError(LoginFailed);
422 setErrorText(
QString(
"Login failed, plain login is disabled by the server."));
428 bool authModeSupported =
false;
430 if (d->authMode ==
"PLAIN") {
431 authModeSupported =
true;
434 Q_FOREACH (
const QString &capability, d->capabilities) {
436 if (capability.
mid(5) == d->authMode) {
437 authModeSupported =
true;
442 if (!authModeSupported) {
443 setError(LoginFailed);
444 setErrorText(
QString(
"Login failed, authentication mode %1 is not supported by the server.").arg(d->authMode));
446 }
else if (!d->startAuthentication()) {
452 case LoginJobPrivate::Authenticate:
453 sasl_dispose(&d->conn);
455 case LoginJobPrivate::Login:
456 d->saveServerGreeting(response);
464 if (code == MALFORMED) {
465 setErrorText(
QString(
"%1 failed, malformed reply from the server.").arg(commandName));
470 bool LoginJobPrivate::startAuthentication()
474 q->setError(LoginFailed);
475 q->setErrorText(
QString(
"Login failed, client cannot initialize the SASL library."));
479 authState = LoginJobPrivate::Authenticate;
480 const char *out = Q_NULLPTR;
482 const char *mechusing = Q_NULLPTR;
484 int result = sasl_client_new(
"imap", m_session->hostName().toLatin1(), Q_NULLPTR,
nullptr, callbacks, 0, &conn);
485 if (result != SASL_OK) {
486 qCWarning(KIMAP2_LOG) <<
"sasl_client_new failed with:" << result;
487 q->setError(LoginFailed);
493 result = sasl_client_start(conn, authMode.toLatin1(), &client_interact,
capabilities.contains(QStringLiteral(
"SASL-IR")) ? &out : Q_NULLPTR, &outlen, &mechusing);
495 if (result == SASL_INTERACT) {
496 if (!sasl_interact()) {
498 q->setError(LoginFailed);
499 q->setErrorText(
QString(
"sasl_interact failed"));
503 }
while (result == SASL_INTERACT);
505 if (result != SASL_CONTINUE && result != SASL_OK) {
506 qCWarning(KIMAP2_LOG) <<
"sasl_client_start failed with:" << result;
507 q->setError(LoginFailed);
519 sendCommand(
"AUTHENTICATE", authMode.toLatin1() +
' ' + challenge);
525 void LoginJobPrivate::sendPlainLogin()
527 authState = LoginJobPrivate::Login;
528 qCDebug(KIMAP2_LOG) <<
"sending LOGIN";
530 '"' +
quoteIMAP(userName).toUtf8() +
'"' +
532 '"' +
quoteIMAP(password).toUtf8() +
'"');
535 bool LoginJobPrivate::answerChallenge(
const QByteArray &data)
539 const char *out = Q_NULLPTR;
542 result = sasl_client_step(conn, challenge.
isEmpty() ? Q_NULLPTR : challenge.
data(),
547 if (result == SASL_INTERACT) {
548 if (!sasl_interact()) {
549 q->setError(LoginFailed);
550 q->setErrorText(
QString(
"sasl_interact failed"));
555 }
while (result == SASL_INTERACT);
557 if (result != SASL_CONTINUE && result != SASL_OK) {
558 qCWarning(KIMAP2_LOG) <<
"sasl_client_step failed with:" << result;
559 q->setError(LoginFailed);
568 sessionInternal()->sendData(challenge);
576 d->encryptionMode = mode;
577 d->startTls = startTls;
583 return d->encryptionMode;
586 void LoginJob::setAuthenticationMode(AuthenticationMode mode)
592 case Login: d->authMode = QStringLiteral(
"LOGIN");
594 case Plain: d->authMode = QStringLiteral(
"PLAIN");
596 case CramMD5: d->authMode = QStringLiteral(
"CRAM-MD5");
598 case DigestMD5: d->authMode = QStringLiteral(
"DIGEST-MD5");
600 case GSSAPI: d->authMode = QStringLiteral(
"GSSAPI");
602 case Anonymous: d->authMode = QStringLiteral(
"ANONYMOUS");
604 case XOAuth2: d->authMode = QStringLiteral(
"XOAUTH2");
607 d->authMode = QStringLiteral(
"");
611 void LoginJob::connectionLost()
615 qCWarning(KIMAP2_LOG) <<
"Connection to server lost " << d->m_socketError;
617 setError(SslHandshakeFailed);
621 setError(HostNotFound);
625 setError(CouldNotConnect);
631 void LoginJobPrivate::saveServerGreeting(
const Message &response)
636 for (
int i = 2; i < response.content.size(); i++) {
637 if (response.content.at(i).type() == Message::Part::List) {
639 foreach (
const QByteArray &item, response.content.
at(i).toList()) {
642 serverGreeting.chop(1);
643 serverGreeting += QStringLiteral(
") ");
648 serverGreeting.chop(1);
651 QString LoginJob::serverGreeting()
const
654 return d->serverGreeting;
657 #include "moc_loginjob.cpp"