1 | /* |
2 | Copyright (c) 2009 Kevin Ottens <ervin@kde.org> |
3 | Copyright (c) 2009 Andras Mantia <amantia@kde.org> |
4 | |
5 | This library is free software; you can redistribute it and/or modify it |
6 | under the terms of the GNU Library General Public License as published by |
7 | the Free Software Foundation; either version 2 of the License, or (at your |
8 | option) any later version. |
9 | |
10 | This library is distributed in the hope that it will be useful, but WITHOUT |
11 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
12 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public |
13 | License for more details. |
14 | |
15 | You should have received a copy of the GNU Library General Public License |
16 | along with this library; see the file COPYING.LIB. If not, write to the |
17 | Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
18 | 02110-1301, USA. |
19 | */ |
20 | |
21 | #include "loginjob.h" |
22 | |
23 | #include <KDE/KLocalizedString> |
24 | #include <KDE/KDebug> |
25 | #include <ktcpsocket.h> |
26 | |
27 | #include "job_p.h" |
28 | #include "message_p.h" |
29 | #include "session_p.h" |
30 | #include "rfccodecs.h" |
31 | |
32 | #include "common.h" |
33 | |
34 | extern "C" { |
35 | #include <sasl/sasl.h> |
36 | } |
37 | |
38 | static sasl_callback_t callbacks[] = { |
39 | { SASL_CB_ECHOPROMPT, NULL, NULL }, |
40 | { SASL_CB_NOECHOPROMPT, NULL, NULL }, |
41 | { SASL_CB_GETREALM, NULL, NULL }, |
42 | { SASL_CB_USER, NULL, NULL }, |
43 | { SASL_CB_AUTHNAME, NULL, NULL }, |
44 | { SASL_CB_PASS, NULL, NULL }, |
45 | { SASL_CB_CANON_USER, NULL, NULL }, |
46 | { SASL_CB_LIST_END, NULL, NULL } |
47 | }; |
48 | |
49 | namespace KIMAP |
50 | { |
51 | class LoginJobPrivate : public JobPrivate |
52 | { |
53 | public: |
54 | enum AuthState { |
55 | StartTls = 0, |
56 | Capability, |
57 | Login, |
58 | Authenticate |
59 | }; |
60 | |
61 | LoginJobPrivate( LoginJob *job, Session *session, const QString& name ) : JobPrivate( session, name ), q( job ), encryptionMode( LoginJob::Unencrypted ), authState( Login ), plainLoginDisabled( false ) { |
62 | conn = 0; |
63 | client_interact = 0; |
64 | } |
65 | ~LoginJobPrivate() { } |
66 | bool sasl_interact(); |
67 | |
68 | bool startAuthentication(); |
69 | bool answerChallenge(const QByteArray &data); |
70 | void sslResponse(bool response); |
71 | void saveServerGreeting(const Message &response); |
72 | |
73 | LoginJob *q; |
74 | |
75 | QString userName; |
76 | QString authorizationName; |
77 | QString password; |
78 | QString serverGreeting; |
79 | |
80 | LoginJob::EncryptionMode encryptionMode; |
81 | QString authMode; |
82 | AuthState authState; |
83 | QStringList capabilities; |
84 | bool plainLoginDisabled; |
85 | |
86 | sasl_conn_t *conn; |
87 | sasl_interact_t *client_interact; |
88 | }; |
89 | } |
90 | |
91 | using namespace KIMAP; |
92 | |
93 | bool LoginJobPrivate::sasl_interact() |
94 | { |
95 | kDebug() << "sasl_interact" ; |
96 | sasl_interact_t *interact = client_interact; |
97 | |
98 | //some mechanisms do not require username && pass, so it doesn't need a popup |
99 | //window for getting this info |
100 | for ( ; interact->id != SASL_CB_LIST_END; interact++ ) { |
101 | if ( interact->id == SASL_CB_AUTHNAME || |
102 | interact->id == SASL_CB_PASS ) { |
103 | //TODO: dialog for use name?? |
104 | break; |
105 | } |
106 | } |
107 | |
108 | interact = client_interact; |
109 | while ( interact->id != SASL_CB_LIST_END ) { |
110 | kDebug() << "SASL_INTERACT id:" << interact->id; |
111 | switch ( interact->id ) { |
112 | case SASL_CB_AUTHNAME: |
113 | if ( !authorizationName.isEmpty() ) { |
114 | kDebug() << "SASL_CB_[AUTHNAME]: '" << authorizationName << "'" ; |
115 | interact->result = strdup( authorizationName.toUtf8() ); |
116 | interact->len = strlen( (const char *) interact->result ); |
117 | break; |
118 | } |
119 | case SASL_CB_USER: |
120 | kDebug() << "SASL_CB_[USER|AUTHNAME]: '" << userName << "'" ; |
121 | interact->result = strdup( userName.toUtf8() ); |
122 | interact->len = strlen( (const char *) interact->result ); |
123 | break; |
124 | case SASL_CB_PASS: |
125 | kDebug() << "SASL_CB_PASS: [hidden]" ; |
126 | interact->result = strdup( password.toUtf8() ); |
127 | interact->len = strlen( (const char *) interact->result ); |
128 | break; |
129 | default: |
130 | interact->result = 0; |
131 | interact->len = 0; |
132 | break; |
133 | } |
134 | interact++; |
135 | } |
136 | return true; |
137 | } |
138 | |
139 | LoginJob::LoginJob( Session *session ) |
140 | : Job( *new LoginJobPrivate( this, session, i18n( "Login" ) ) ) |
141 | { |
142 | Q_D( LoginJob ); |
143 | connect( d->sessionInternal(), SIGNAL(encryptionNegotiationResult(bool)), this, SLOT(sslResponse(bool)) ); |
144 | kDebug() << this; |
145 | } |
146 | |
147 | LoginJob::~LoginJob() |
148 | { |
149 | kDebug() << this; |
150 | } |
151 | |
152 | QString LoginJob::userName() const |
153 | { |
154 | Q_D( const LoginJob ); |
155 | return d->userName; |
156 | } |
157 | |
158 | void LoginJob::setUserName( const QString &userName ) |
159 | { |
160 | Q_D( LoginJob ); |
161 | d->userName = userName; |
162 | } |
163 | |
164 | QString LoginJob::authorizationName() const |
165 | { |
166 | Q_D( const LoginJob ); |
167 | return d->authorizationName; |
168 | } |
169 | |
170 | void LoginJob::setAuthorizationName( const QString& authorizationName ) |
171 | { |
172 | Q_D( LoginJob ); |
173 | d->authorizationName = authorizationName; |
174 | } |
175 | |
176 | QString LoginJob::password() const |
177 | { |
178 | Q_D( const LoginJob ); |
179 | return d->password; |
180 | } |
181 | |
182 | void LoginJob::setPassword( const QString &password ) |
183 | { |
184 | Q_D( LoginJob ); |
185 | d->password = password; |
186 | } |
187 | |
188 | void LoginJob::doStart() |
189 | { |
190 | Q_D( LoginJob ); |
191 | |
192 | kDebug() << this; |
193 | // Don't authenticate on a session in the authenticated state |
194 | if ( session()->state() == Session::Authenticated || session()->state() == Session::Selected ) { |
195 | setError( UserDefinedError ); |
196 | setErrorText( i18n( "IMAP session in the wrong state for authentication" ) ); |
197 | emitResult(); |
198 | return; |
199 | } |
200 | |
201 | // Trigger encryption negotiation only if needed |
202 | EncryptionMode encryptionMode = d->encryptionMode; |
203 | |
204 | switch ( d->sessionInternal()->negotiatedEncryption() ) { |
205 | case KTcpSocket::UnknownSslVersion: |
206 | break; // Do nothing the encryption mode still needs to be negotiated |
207 | |
208 | // For the other cases, pretend we're going unencrypted as that's the |
209 | // encryption mode already set on the session |
210 | // (so for instance we won't issue another STARTTLS for nothing if that's |
211 | // not needed) |
212 | case KTcpSocket::SslV2: |
213 | if ( encryptionMode == SslV2 ) { |
214 | encryptionMode = Unencrypted; |
215 | } |
216 | break; |
217 | case KTcpSocket::SslV3: |
218 | if ( encryptionMode == SslV3 ) { |
219 | encryptionMode = Unencrypted; |
220 | } |
221 | break; |
222 | case KTcpSocket::TlsV1: |
223 | if ( encryptionMode == TlsV1 ) { |
224 | encryptionMode = Unencrypted; |
225 | } |
226 | break; |
227 | case KTcpSocket::AnySslVersion: |
228 | if ( encryptionMode == AnySslVersion ) { |
229 | encryptionMode = Unencrypted; |
230 | } |
231 | break; |
232 | } |
233 | |
234 | if ( encryptionMode == SslV2 || |
235 | encryptionMode == SslV3 || |
236 | encryptionMode == SslV3_1 || |
237 | encryptionMode == AnySslVersion ) { |
238 | KTcpSocket::SslVersion version = KTcpSocket::SslV2; |
239 | if ( encryptionMode == SslV3 ) { |
240 | version = KTcpSocket::SslV3; |
241 | } |
242 | if ( encryptionMode == SslV3_1 ) { |
243 | version = KTcpSocket::SslV3_1; |
244 | } |
245 | if ( encryptionMode == AnySslVersion ) { |
246 | version = KTcpSocket::AnySslVersion; |
247 | } |
248 | d->sessionInternal()->startSsl( version ); |
249 | |
250 | } else if ( encryptionMode == TlsV1 ) { |
251 | d->authState = LoginJobPrivate::StartTls; |
252 | d->tags << d->sessionInternal()->sendCommand( "STARTTLS" ); |
253 | |
254 | } else if ( encryptionMode == Unencrypted ) { |
255 | if ( d->authMode.isEmpty() ) { |
256 | d->authState = LoginJobPrivate::Login; |
257 | kDebug() << "sending LOGIN" ; |
258 | d->tags << d->sessionInternal()->sendCommand( "LOGIN" , |
259 | '"' + quoteIMAP( d->userName ).toUtf8() + '"' + |
260 | ' ' + |
261 | '"' + quoteIMAP( d->password ).toUtf8() + '"' ); |
262 | } else { |
263 | if ( !d->startAuthentication() ) { |
264 | emitResult(); |
265 | } |
266 | } |
267 | } |
268 | } |
269 | |
270 | void LoginJob::handleResponse( const Message &response ) |
271 | { |
272 | Q_D( LoginJob ); |
273 | |
274 | if ( response.content.isEmpty() ) { |
275 | return; |
276 | } |
277 | |
278 | //set the actual command name for standard responses |
279 | QString commandName = i18n( "Login" ); |
280 | if ( d->authState == LoginJobPrivate::Capability ) { |
281 | commandName = i18n( "Capability" ); |
282 | } else if ( d->authState == LoginJobPrivate::StartTls ) { |
283 | commandName = i18n( "StartTls" ); |
284 | } |
285 | |
286 | enum ResponseCode { |
287 | OK, |
288 | ERR, |
289 | UNTAGGED, |
290 | CONTINUATION, |
291 | MALFORMED |
292 | }; |
293 | |
294 | QByteArray tag = response.content.first().toString(); |
295 | ResponseCode code = OK; |
296 | |
297 | kDebug() << commandName << tag; |
298 | |
299 | if ( tag == "+" ) { |
300 | code = CONTINUATION; |
301 | } else if ( tag == "*" ) { |
302 | if ( response.content.size() < 2 ) { |
303 | code = MALFORMED; // Received empty untagged response |
304 | } else { |
305 | code = UNTAGGED; |
306 | } |
307 | } else if ( d->tags.contains( tag ) ) { |
308 | if ( response.content.size() < 2 ) { |
309 | code = MALFORMED; |
310 | } else if ( response.content[1].toString() == "OK" ) { |
311 | code = OK; |
312 | } else { |
313 | code = ERR; |
314 | } |
315 | } |
316 | |
317 | switch ( code ) { |
318 | case MALFORMED: |
319 | // We'll handle it later |
320 | break; |
321 | |
322 | case ERR: |
323 | //server replied with NO or BAD for SASL authentication |
324 | if ( d->authState == LoginJobPrivate::Authenticate ) { |
325 | sasl_dispose( &d->conn ); |
326 | } |
327 | |
328 | setError( UserDefinedError ); |
329 | setErrorText( i18n( "%1 failed, server replied: %2" , commandName, QLatin1String(response.toString().constData()) ) ); |
330 | emitResult(); |
331 | return; |
332 | |
333 | case UNTAGGED: |
334 | // The only untagged response interesting for us here is CAPABILITY |
335 | if ( response.content[1].toString() == "CAPABILITY" ) { |
336 | QList<Message::Part>::const_iterator p = response.content.begin() + 2; |
337 | while ( p != response.content.end() ) { |
338 | QString capability = QLatin1String(p->toString()); |
339 | d->capabilities << capability; |
340 | if ( capability == QLatin1String("LOGINDISABLED" ) ) { |
341 | d->plainLoginDisabled = true; |
342 | } |
343 | ++p; |
344 | } |
345 | kDebug() << "Capabilities updated: " << d->capabilities; |
346 | } |
347 | break; |
348 | |
349 | case CONTINUATION: |
350 | if ( d->authState != LoginJobPrivate::Authenticate ) { |
351 | // Received unexpected continuation response for something |
352 | // other than AUTHENTICATE command |
353 | code = MALFORMED; |
354 | break; |
355 | } |
356 | |
357 | if ( d->authMode == QLatin1String( "PLAIN" ) ) { |
358 | if ( response.content.size()>1 && response.content.at( 1 ).toString() == "OK" ) { |
359 | return; |
360 | } |
361 | QByteArray challengeResponse; |
362 | if ( !d->authorizationName.isEmpty() ) { |
363 | challengeResponse+= d->authorizationName.toUtf8(); |
364 | } |
365 | challengeResponse+= '\0'; |
366 | challengeResponse+= d->userName.toUtf8(); |
367 | challengeResponse+= '\0'; |
368 | challengeResponse+= d->password.toUtf8(); |
369 | challengeResponse = challengeResponse.toBase64(); |
370 | d->sessionInternal()->sendData( challengeResponse ); |
371 | } else if ( response.content.size() >= 2 ) { |
372 | if ( !d->answerChallenge( QByteArray::fromBase64( response.content[1].toString() ) ) ) { |
373 | emitResult(); //error, we're done |
374 | } |
375 | } else { |
376 | // Received empty continuation for authMode other than PLAIN |
377 | code = MALFORMED; |
378 | } |
379 | break; |
380 | |
381 | case OK: |
382 | |
383 | switch ( d->authState ) { |
384 | case LoginJobPrivate::StartTls: |
385 | d->sessionInternal()->startSsl( KTcpSocket::TlsV1 ); |
386 | break; |
387 | |
388 | case LoginJobPrivate::Capability: |
389 | //cleartext login, if enabled |
390 | if ( d->authMode.isEmpty() ) { |
391 | if ( d->plainLoginDisabled ) { |
392 | setError( UserDefinedError ); |
393 | setErrorText( i18n( "Login failed, plain login is disabled by the server." ) ); |
394 | emitResult(); |
395 | } else { |
396 | d->authState = LoginJobPrivate::Login; |
397 | d->tags << d->sessionInternal()->sendCommand( "LOGIN" , |
398 | '"' + quoteIMAP( d->userName ).toUtf8() + '"' + |
399 | ' ' + |
400 | '"' + quoteIMAP( d->password ).toUtf8() + '"' ); |
401 | } |
402 | } else { |
403 | bool authModeSupported = false; |
404 | //find the selected SASL authentication method |
405 | Q_FOREACH ( const QString &capability, d->capabilities ) { |
406 | if ( capability.startsWith( QLatin1String( "AUTH=" ) ) ) { |
407 | if ( capability.mid( 5 ) == d->authMode ) { |
408 | authModeSupported = true; |
409 | break; |
410 | } |
411 | } |
412 | } |
413 | if ( !authModeSupported ) { |
414 | setError( UserDefinedError ); |
415 | setErrorText( i18n( "Login failed, authentication mode %1 is not supported by the server." , d->authMode ) ); |
416 | emitResult(); |
417 | } else if ( !d->startAuthentication() ) { |
418 | emitResult(); //problem, we're done |
419 | } |
420 | } |
421 | break; |
422 | |
423 | case LoginJobPrivate::Authenticate: |
424 | sasl_dispose( &d->conn ); //SASL authentication done |
425 | // Fall through |
426 | case LoginJobPrivate::Login: |
427 | d->saveServerGreeting( response ); |
428 | emitResult(); //got an OK, command done |
429 | break; |
430 | |
431 | } |
432 | |
433 | } |
434 | |
435 | if ( code == MALFORMED ) { |
436 | setErrorText( i18n( "%1 failed, malformed reply from the server." , commandName ) ); |
437 | emitResult(); |
438 | } |
439 | } |
440 | |
441 | bool LoginJobPrivate::startAuthentication() |
442 | { |
443 | //SASL authentication |
444 | if ( !initSASL() ) { |
445 | q->setError( LoginJob::UserDefinedError ); |
446 | q->setErrorText( i18n( "Login failed, client cannot initialize the SASL library." ) ); |
447 | return false; |
448 | } |
449 | |
450 | authState = LoginJobPrivate::Authenticate; |
451 | const char *out = 0; |
452 | uint outlen = 0; |
453 | const char *mechusing = 0; |
454 | |
455 | int result = sasl_client_new( "imap" , m_session->hostName().toLatin1(), 0, 0, callbacks, 0, &conn ); |
456 | if ( result != SASL_OK ) { |
457 | kDebug() << "sasl_client_new failed with:" << result; |
458 | q->setError( LoginJob::UserDefinedError ); |
459 | q->setErrorText( QString::fromUtf8( sasl_errdetail( conn ) ) ); |
460 | return false; |
461 | } |
462 | |
463 | do { |
464 | result = sasl_client_start( conn, authMode.toLatin1(), &client_interact, capabilities.contains( QLatin1String("SASL-IR" ) ) ? &out : 0, &outlen, &mechusing ); |
465 | |
466 | if ( result == SASL_INTERACT ) { |
467 | if ( !sasl_interact() ) { |
468 | sasl_dispose( &conn ); |
469 | q->setError( LoginJob::UserDefinedError ); //TODO: check up the actual error |
470 | return false; |
471 | } |
472 | } |
473 | } while ( result == SASL_INTERACT ); |
474 | |
475 | if ( result != SASL_CONTINUE && result != SASL_OK ) { |
476 | kDebug() << "sasl_client_start failed with:" << result; |
477 | q->setError( LoginJob::UserDefinedError ); |
478 | q->setErrorText( QString::fromUtf8( sasl_errdetail( conn ) ) ); |
479 | sasl_dispose( &conn ); |
480 | return false; |
481 | } |
482 | |
483 | QByteArray tmp = QByteArray::fromRawData( out, outlen ); |
484 | QByteArray challenge = tmp.toBase64(); |
485 | |
486 | if ( challenge.isEmpty() ) { |
487 | tags << sessionInternal()->sendCommand( "AUTHENTICATE" , authMode.toLatin1() ); |
488 | } else { |
489 | tags << sessionInternal()->sendCommand( "AUTHENTICATE" , authMode.toLatin1() + ' ' + challenge ); |
490 | } |
491 | |
492 | return true; |
493 | } |
494 | |
495 | bool LoginJobPrivate::answerChallenge(const QByteArray &data) |
496 | { |
497 | QByteArray challenge = data; |
498 | int result = -1; |
499 | const char *out = 0; |
500 | uint outlen = 0; |
501 | do { |
502 | result = sasl_client_step( conn, challenge.isEmpty() ? 0 : challenge.data(), |
503 | challenge.size(), |
504 | &client_interact, |
505 | &out, &outlen ); |
506 | |
507 | if ( result == SASL_INTERACT ) { |
508 | if ( !sasl_interact() ) { |
509 | q->setError( LoginJob::UserDefinedError ); //TODO: check up the actual error |
510 | sasl_dispose( &conn ); |
511 | return false; |
512 | } |
513 | } |
514 | } while ( result == SASL_INTERACT ); |
515 | |
516 | if ( result != SASL_CONTINUE && result != SASL_OK ) { |
517 | kDebug() << "sasl_client_step failed with:" << result; |
518 | q->setError( LoginJob::UserDefinedError ); //TODO: check up the actual error |
519 | q->setErrorText( QString::fromUtf8( sasl_errdetail( conn ) ) ); |
520 | sasl_dispose( &conn ); |
521 | return false; |
522 | } |
523 | |
524 | QByteArray tmp = QByteArray::fromRawData( out, outlen ); |
525 | challenge = tmp.toBase64(); |
526 | |
527 | sessionInternal()->sendData( challenge ); |
528 | |
529 | return true; |
530 | } |
531 | |
532 | void LoginJobPrivate::sslResponse(bool response) |
533 | { |
534 | if ( response ) { |
535 | authState = LoginJobPrivate::Capability; |
536 | tags << sessionInternal()->sendCommand( "CAPABILITY" ); |
537 | } else { |
538 | q->setError( LoginJob::UserDefinedError ); |
539 | q->setErrorText( i18n( "Login failed, TLS negotiation failed." ) ); |
540 | encryptionMode = LoginJob::Unencrypted; |
541 | q->emitResult(); |
542 | } |
543 | } |
544 | |
545 | void LoginJob::setEncryptionMode(EncryptionMode mode) |
546 | { |
547 | Q_D( LoginJob ); |
548 | d->encryptionMode = mode; |
549 | } |
550 | |
551 | LoginJob::EncryptionMode LoginJob::encryptionMode() |
552 | { |
553 | Q_D( LoginJob ); |
554 | return d->encryptionMode; |
555 | } |
556 | |
557 | void LoginJob::setAuthenticationMode(AuthenticationMode mode) |
558 | { |
559 | Q_D( LoginJob ); |
560 | switch ( mode ) { |
561 | case ClearText: d->authMode = QLatin1String( "" ); |
562 | break; |
563 | case Login: d->authMode = QLatin1String("LOGIN" ); |
564 | break; |
565 | case Plain: d->authMode = QLatin1String("PLAIN" ); |
566 | break; |
567 | case CramMD5: d->authMode = QLatin1String("CRAM-MD5" ); |
568 | break; |
569 | case DigestMD5: d->authMode = QLatin1String("DIGEST-MD5" ); |
570 | break; |
571 | case GSSAPI: d->authMode = QLatin1String("GSSAPI" ); |
572 | break; |
573 | case Anonymous: d->authMode = QLatin1String("ANONYMOUS" ); |
574 | break; |
575 | case XOAuth2: d->authMode = QLatin1String("XOAUTH2" ); |
576 | break; |
577 | default: |
578 | d->authMode = QLatin1String("" ); |
579 | } |
580 | } |
581 | |
582 | void LoginJob::connectionLost() |
583 | { |
584 | Q_D( LoginJob ); |
585 | |
586 | //don't emit the result if the connection was lost before getting the tls result, as it can mean |
587 | //the TLS handshake failed and the socket was reconnected in normal mode |
588 | if ( d->authState != LoginJobPrivate::StartTls ) { |
589 | kWarning() << "Connection to server lost " << d->m_socketError; |
590 | if ( d->m_socketError == KTcpSocket::SslHandshakeFailedError) { |
591 | setError( KJob::UserDefinedError ); |
592 | setErrorText( i18n( "SSL handshake failed." ) ); |
593 | emitResult(); |
594 | } else { |
595 | setError( ERR_COULD_NOT_CONNECT ); |
596 | setErrorText( i18n( "Connection to server lost." ) ); |
597 | emitResult(); |
598 | } |
599 | } |
600 | |
601 | } |
602 | |
603 | void LoginJobPrivate::saveServerGreeting(const Message &response) |
604 | { |
605 | // Concatenate the parts of the server response into a string, while dropping the first two parts |
606 | // (the response tag and the "OK" code), and being careful not to add useless extra whitespace. |
607 | |
608 | for ( int i = 2; i < response.content.size(); i++ ) { |
609 | if ( response.content.at( i ).type() == Message::Part::List ) { |
610 | serverGreeting += QLatin1Char('('); |
611 | foreach ( const QByteArray &item, response.content.at( i ).toList() ) { |
612 | serverGreeting += QLatin1String(item) + QLatin1Char(' '); |
613 | } |
614 | serverGreeting.chop( 1 ); |
615 | serverGreeting += QLatin1String(") " ); |
616 | } else { |
617 | serverGreeting+= QLatin1String(response.content.at( i ).toString()) + QLatin1Char(' '); |
618 | } |
619 | } |
620 | serverGreeting.chop( 1 ); |
621 | } |
622 | |
623 | QString LoginJob::serverGreeting() const |
624 | { |
625 | Q_D( const LoginJob ); |
626 | return d->serverGreeting; |
627 | } |
628 | |
629 | #include "moc_loginjob.cpp" |
630 | |