24 #include <KDE/KLocale>
26 #include <ktcpsocket.h>
29 #include "message_p.h"
30 #include "session_p.h"
36 #include <sasl/sasl.h>
39 static sasl_callback_t callbacks[] = {
40 { SASL_CB_ECHOPROMPT, NULL, NULL },
41 { SASL_CB_NOECHOPROMPT, NULL, NULL },
42 { SASL_CB_GETREALM, NULL, NULL },
43 { SASL_CB_USER, NULL, NULL },
44 { SASL_CB_AUTHNAME, NULL, NULL },
45 { SASL_CB_PASS, NULL, NULL },
46 { SASL_CB_CANON_USER, NULL, NULL },
47 { SASL_CB_LIST_END, NULL, NULL }
52 class LoginJobPrivate :
public JobPrivate
62 LoginJobPrivate( LoginJob *job, Session *session,
const QString& name ) : JobPrivate(session, name), q(job), encryptionMode(LoginJob::Unencrypted), authState(Login), plainLoginDisabled(false) {
66 ~LoginJobPrivate() { }
69 bool startAuthentication();
70 bool answerChallenge(
const QByteArray &data);
71 void sslResponse(
bool response);
72 void saveServerGreeting(
const Message &response);
78 QString serverGreeting;
80 LoginJob::EncryptionMode encryptionMode;
83 QStringList capabilities;
84 bool plainLoginDisabled;
87 sasl_interact_t *client_interact;
91 using namespace KIMAP;
93 bool LoginJobPrivate::sasl_interact()
95 kDebug() <<
"sasl_interact";
96 sasl_interact_t *interact = client_interact;
100 for ( ; interact->id != SASL_CB_LIST_END; interact++ ) {
101 if ( interact->id == SASL_CB_AUTHNAME ||
102 interact->id == SASL_CB_PASS ) {
108 interact = client_interact;
109 while( interact->id != SASL_CB_LIST_END ) {
110 kDebug() <<
"SASL_INTERACT id:" << interact->id;
111 switch( interact->id ) {
113 case SASL_CB_AUTHNAME:
114 kDebug() <<
"SASL_CB_[USER|AUTHNAME]: '" << userName <<
"'";
115 interact->result = strdup( userName.toUtf8() );
116 interact->len = strlen( (
const char *) interact->result );
119 kDebug() <<
"SASL_CB_PASS: [hidden]";
120 interact->result = strdup( password.toUtf8() );
121 interact->len = strlen( (
const char *) interact->result );
124 interact->result = 0;
134 LoginJob::LoginJob( Session *session )
135 : Job( *new LoginJobPrivate(this, session, i18n(
"Login")) )
138 connect(d->sessionInternal(), SIGNAL(encryptionNegotiationResult(
bool)),
this, SLOT(sslResponse(
bool)));
141 LoginJob::~LoginJob()
145 QString LoginJob::userName()
const
151 void LoginJob::setUserName(
const QString &userName )
154 d->userName = userName;
157 QString LoginJob::password()
const
163 void LoginJob::setPassword(
const QString &password )
166 d->password = password;
169 void LoginJob::doStart()
174 if ( session()->state() == Session::Authenticated || session()->state() == Session::Selected ) {
175 setError( UserDefinedError );
176 setErrorText( i18n(
"IMAP session in the wrong state for authentication") );
182 EncryptionMode encryptionMode = d->encryptionMode;
184 switch ( d->sessionInternal()->negotiatedEncryption() ) {
185 case KTcpSocket::UnknownSslVersion:
192 case KTcpSocket::SslV2:
193 if ( encryptionMode==SslV2 ) {
194 encryptionMode = Unencrypted;
197 case KTcpSocket::SslV3:
198 if ( encryptionMode==SslV3 ) {
199 encryptionMode = Unencrypted;
202 case KTcpSocket::TlsV1:
203 if ( encryptionMode==TlsV1 ) {
204 encryptionMode = Unencrypted;
207 case KTcpSocket::AnySslVersion:
208 if ( encryptionMode==AnySslVersion ) {
209 encryptionMode = Unencrypted;
214 if (encryptionMode == SslV2
215 || encryptionMode == SslV3
216 || encryptionMode == SslV3_1
217 || encryptionMode == AnySslVersion) {
218 KTcpSocket::SslVersion version = KTcpSocket::SslV2;
219 if (encryptionMode == SslV3)
220 version = KTcpSocket::SslV3;
221 if (encryptionMode == SslV3_1)
222 version = KTcpSocket::SslV3_1;
223 if (encryptionMode == AnySslVersion)
224 version = KTcpSocket::AnySslVersion;
225 d->sessionInternal()->startSsl(version);
227 }
else if (encryptionMode == TlsV1) {
228 d->authState = LoginJobPrivate::StartTls;
229 d->tags << d->sessionInternal()->sendCommand(
"STARTTLS" );
231 }
else if (encryptionMode == Unencrypted ) {
232 if (d->authMode.isEmpty()) {
233 d->authState = LoginJobPrivate::Login;
234 d->tags << d->sessionInternal()->sendCommand(
"LOGIN",
235 '"'+quoteIMAP( d->userName ).toUtf8()+
'"'
237 +
'"'+quoteIMAP(d->password ).toUtf8()+
'"' );
239 if (!d->startAuthentication()) {
246 void LoginJob::handleResponse(
const Message &response )
250 if ( response.content.isEmpty() )
254 QString commandName = i18n(
"Login");
255 if (d->authState == LoginJobPrivate::Capability) {
256 commandName = i18n(
"Capability");
257 }
else if (d->authState == LoginJobPrivate::StartTls) {
258 commandName = i18n(
"StartTls");
269 QByteArray tag = response.content.first().toString();
274 }
else if ( tag ==
"*" ) {
275 if ( response.content.size() < 2 )
279 }
else if ( d->tags.contains(tag) ) {
280 if ( response.content.size() < 2 )
282 else if ( response.content[1].toString() ==
"OK" )
295 if (d->authState == LoginJobPrivate::Authenticate)
296 sasl_dispose( &d->conn );
298 setError( UserDefinedError );
299 setErrorText( i18n(
"%1 failed, server replied: %2", commandName, response.toString().constData()) );
305 if ( response.content[1].toString() ==
"CAPABILITY" ) {
306 QList<Message::Part>::const_iterator p = response.content.begin() + 2;
307 while (p != response.content.end()) {
308 QString capability = p->toString();
309 d->capabilities << capability;
310 if (capability ==
"LOGINDISABLED")
311 d->plainLoginDisabled =
true;
314 kDebug() <<
"Capabilities updated: " << d->capabilities;
319 if (d->authState != LoginJobPrivate::Authenticate) {
326 if ( d->authMode == QLatin1String(
"PLAIN" ) ) {
327 if ( response.content.size()>1 && response.content.at( 1 ).toString()==
"OK" ) {
330 QByteArray challengeResponse;
331 challengeResponse+=
'\0';
332 challengeResponse+= d->userName.toUtf8();
333 challengeResponse+=
'\0';
334 challengeResponse+= d->password.toUtf8();
335 challengeResponse = challengeResponse.toBase64();
336 d->sessionInternal()->sendData( challengeResponse );
337 }
else if ( response.content.size() >= 2 ) {
338 if (!d->answerChallenge(QByteArray::fromBase64(response.content[1].toString()))) {
349 switch (d->authState) {
350 case LoginJobPrivate::StartTls:
351 d->sessionInternal()->startSsl(KTcpSocket::TlsV1);
354 case LoginJobPrivate::Capability:
356 if (d->authMode.isEmpty()) {
357 if (d->plainLoginDisabled) {
358 setError( UserDefinedError );
359 setErrorText( i18n(
"Login failed, plain login is disabled by the server.") );
362 d->authState = LoginJobPrivate::Login;
363 d->tags << d->sessionInternal()->sendCommand(
"LOGIN",
364 '"'+quoteIMAP( d->userName ).toUtf8()+
'"'
366 +
'"'+quoteIMAP( d->password ).toUtf8()+
'"');
369 bool authModeSupported =
false;
371 Q_FOREACH(
const QString &capability, d->capabilities) {
372 if (capability.startsWith(QLatin1String(
"AUTH="))) {
373 if (capability.mid(5) == d->authMode) {
374 authModeSupported =
true;
379 if (!authModeSupported) {
380 setError( UserDefinedError );
381 setErrorText( i18n(
"Login failed, authentication mode %1 is not supported by the server.", d->authMode) );
383 }
else if (!d->startAuthentication()) {
389 case LoginJobPrivate::Authenticate:
390 sasl_dispose( &d->conn );
392 case LoginJobPrivate::Login:
393 d->saveServerGreeting( response );
401 if ( code == MALFORMED ) {
402 setErrorText( i18n(
"%1 failed, malformed reply from the server.", commandName) );
407 bool LoginJobPrivate::startAuthentication()
411 q->setError( LoginJob::UserDefinedError );
412 q->setErrorText( i18n(
"Login failed, client cannot initialize the SASL library.") );
416 authState = LoginJobPrivate::Authenticate;
419 const char *mechusing = 0;
421 int result = sasl_client_new(
"imap", m_session->hostName().toLatin1(), 0, 0, callbacks, 0, &conn );
422 if ( result != SASL_OK ) {
423 kDebug() <<
"sasl_client_new failed with:" << result;
424 q->setError( LoginJob::UserDefinedError );
425 q->setErrorText( QString::fromUtf8( sasl_errdetail( conn ) ) );
430 result = sasl_client_start(conn, authMode.toLatin1(), &client_interact, capabilities.contains(
"SASL-IR") ? &out : 0, &outlen, &mechusing);
432 if ( result == SASL_INTERACT ) {
433 if ( !sasl_interact() ) {
434 sasl_dispose( &conn );
435 q->setError( LoginJob::UserDefinedError );
439 }
while ( result == SASL_INTERACT );
441 if ( result != SASL_CONTINUE && result != SASL_OK ) {
442 kDebug() <<
"sasl_client_start failed with:" << result;
443 q->setError( LoginJob::UserDefinedError );
444 q->setErrorText( QString::fromUtf8( sasl_errdetail( conn ) ) );
445 sasl_dispose( &conn );
449 QByteArray tmp = QByteArray::fromRawData( out, outlen );
450 QByteArray challenge = tmp.toBase64();
452 if ( challenge.isEmpty() ) {
453 tags << sessionInternal()->sendCommand(
"AUTHENTICATE", authMode.toLatin1() );
455 tags << sessionInternal()->sendCommand(
"AUTHENTICATE", authMode.toLatin1() +
' ' + challenge );
461 bool LoginJobPrivate::answerChallenge(
const QByteArray &data)
463 QByteArray challenge = data;
468 result = sasl_client_step(conn, challenge.isEmpty() ? 0 : challenge.data(),
473 if (result == SASL_INTERACT) {
474 if ( !sasl_interact() ) {
475 q->setError( LoginJob::UserDefinedError );
476 sasl_dispose( &conn );
480 }
while ( result == SASL_INTERACT );
482 if ( result != SASL_CONTINUE && result != SASL_OK ) {
483 kDebug() <<
"sasl_client_step failed with:" << result;
484 q->setError( LoginJob::UserDefinedError );
485 q->setErrorText( QString::fromUtf8( sasl_errdetail( conn ) ) );
486 sasl_dispose( &conn );
490 QByteArray tmp = QByteArray::fromRawData( out, outlen );
491 challenge = tmp.toBase64();
493 sessionInternal()->sendData( challenge );
498 void LoginJobPrivate::sslResponse(
bool response)
501 authState = LoginJobPrivate::Capability;
502 tags << sessionInternal()->sendCommand(
"CAPABILITY" );
504 q->setError( LoginJob::UserDefinedError );
505 q->setErrorText( i18n(
"Login failed, TLS negotiation failed." ));
506 encryptionMode = LoginJob::Unencrypted;
511 void LoginJob::setEncryptionMode(EncryptionMode mode)
514 d->encryptionMode = mode;
517 LoginJob::EncryptionMode LoginJob::encryptionMode()
520 return d->encryptionMode;
523 void LoginJob::setAuthenticationMode(AuthenticationMode mode)
528 case ClearText: d->authMode =
"";
530 case Login: d->authMode =
"LOGIN";
532 case Plain: d->authMode =
"PLAIN";
534 case CramMD5: d->authMode =
"CRAM-MD5";
536 case DigestMD5: d->authMode =
"DIGEST-MD5";
538 case GSSAPI: d->authMode =
"GSSAPI";
540 case Anonymous: d->authMode =
"ANONYMOUS";
547 void LoginJob::connectionLost()
553 if (d->authState != LoginJobPrivate::StartTls) {
554 setError( ERR_COULD_NOT_CONNECT );
555 setErrorText( i18n(
"Connection to server lost.") );
561 void LoginJobPrivate::saveServerGreeting(
const Message &response)
566 for (
int i=2; i<response.content.size(); i++) {
567 if ( response.content.at(i).type()==Message::Part::List ) {
569 foreach (
const QByteArray &item, response.content.at(i).toList() ) {
570 serverGreeting+=item+
' ';
572 serverGreeting.chop(1);
573 serverGreeting+=
") ";
575 serverGreeting+=response.content.at(i).toString()+
' ';
578 serverGreeting.chop(1);
581 QString LoginJob::serverGreeting()
const
584 return d->serverGreeting;
587 #include "loginjob.moc"