smtpsession.cpp
00001 /* 00002 Copyright (c) 2010 Volker Krause <vkrause@kde.org> 00003 00004 This library is free software; you can redistribute it and/or modify it 00005 under the terms of the GNU Library General Public License as published by 00006 the Free Software Foundation; either version 2 of the License, or (at your 00007 option) any later version. 00008 00009 This library is distributed in the hope that it will be useful, but WITHOUT 00010 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 00011 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public 00012 License for more details. 00013 00014 You should have received a copy of the GNU Library General Public License 00015 along with this library; see the file COPYING.LIB. If not, write to the 00016 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 00017 02110-1301, USA. 00018 */ 00019 00020 #include "smtpsession.h" 00021 00022 #include "common.h" 00023 #include "smtp/smtpsessioninterface.h" 00024 #include "smtp/request.h" 00025 #include "smtp/response.h" 00026 #include "smtp/command.h" 00027 #include "smtp/transactionstate.h" 00028 00029 #include <ktcpsocket.h> 00030 #include <KMessageBox> 00031 #include <KIO/PasswordDialog> 00032 #include <kio/authinfo.h> 00033 #include <kio/global.h> 00034 #include <kio/sslui.h> 00035 #include <KLocalizedString> 00036 #include <kpimutils/networkaccesshelper.h> 00037 #include <KDebug> 00038 00039 #include <QtCore/QQueue> 00040 #include <QtNetwork/QHostInfo> 00041 00042 using namespace MailTransport; 00043 using namespace KioSMTP; 00044 00045 class MailTransport::SmtpSessionPrivate : public KioSMTP::SMTPSessionInterface 00046 { 00047 public: 00048 explicit SmtpSessionPrivate( SmtpSession *session ) : 00049 useTLS( true ), 00050 socket( 0 ), 00051 currentCommand( 0 ), 00052 currentTransactionState( 0 ), 00053 state( Initial ), 00054 q( session ) 00055 {} 00056 00057 void dataReq() { /* noop */ }; 00058 int readData( QByteArray &ba ) 00059 { 00060 if ( data->atEnd() ) { 00061 ba.clear(); 00062 return 0; 00063 } else { 00064 Q_ASSERT( data->isOpen() ); 00065 ba = data->read( 32 * 1024 ); 00066 return ba.size(); 00067 } 00068 } 00069 00070 void error( int id, const QString &msg ) 00071 { 00072 kDebug() << id << msg; 00073 // clear state so further replies don't end up in failed commands etc. 00074 currentCommand = 0; 00075 currentTransactionState = 0; 00076 00077 if ( errorMessage.isEmpty() ) { 00078 errorMessage = KIO::buildErrorString( id, msg ); 00079 } 00080 q->disconnectFromHost(); 00081 } 00082 00083 void informationMessageBox( const QString &msg, const QString &caption ) 00084 { 00085 KMessageBox::information( 0, msg, caption ); 00086 } 00087 00088 bool openPasswordDialog( KIO::AuthInfo &authInfo ) { 00089 return KIO::PasswordDialog::getNameAndPassword( 00090 authInfo.username, 00091 authInfo.password, 00092 &( authInfo.keepPassword ), 00093 authInfo.prompt, 00094 authInfo.readOnly, 00095 authInfo.caption, 00096 authInfo.comment, 00097 authInfo.commentLabel ) == KIO::PasswordDialog::Accepted; 00098 } 00099 00100 bool startSsl() 00101 { 00102 kDebug(); 00103 Q_ASSERT( socket ); 00104 socket->setAdvertisedSslVersion( KTcpSocket::TlsV1 ); 00105 socket->ignoreSslErrors(); 00106 socket->startClientEncryption(); 00107 const bool encrypted = socket->waitForEncrypted( 60 * 1000 ); 00108 00109 const KSslCipher cipher = socket->sessionCipher(); 00110 if ( !encrypted || 00111 socket->sslErrors().count() > 0 || 00112 socket->encryptionMode() != KTcpSocket::SslClientMode || 00113 cipher.isNull() || 00114 cipher.usedBits() == 0 ) { 00115 kDebug() << "Initial SSL handshake failed. cipher.isNull() is" << cipher.isNull() 00116 << ", cipher.usedBits() is" << cipher.usedBits() 00117 << ", the socket says:" << socket->errorString() 00118 << "and the list of SSL errors contains" 00119 << socket->sslErrors().count() << "items."; 00120 00121 if ( KIO::SslUi::askIgnoreSslErrors( socket ) ) { 00122 return true; 00123 } else { 00124 return false; 00125 } 00126 } else { 00127 kDebug() << "TLS negotiation done."; 00128 return true; 00129 } 00130 } 00131 00132 bool lf2crlfAndDotStuffingRequested() const { return true; } 00133 QString requestedSaslMethod() const { return saslMethod; } 00134 TLSRequestState tlsRequested() const { return useTLS ? ForceTLS : ForceNoTLS; } 00135 00136 void socketConnected() 00137 { 00138 kDebug(); 00139 if ( destination.protocol() == QLatin1String( "smtps" ) ) { 00140 if ( !startSsl() ) { 00141 error( KIO::ERR_SLAVE_DEFINED, i18n( "SSL negotiation failed." ) ); 00142 } 00143 } 00144 } 00145 00146 void socketDisconnected() 00147 { 00148 kDebug(); 00149 emit q->result( q ); 00150 q->deleteLater(); 00151 } 00152 00153 void socketError( KTcpSocket::Error err ) 00154 { 00155 kDebug() << err; 00156 error( KIO::ERR_CONNECTION_BROKEN, socket->errorString() ); 00157 00158 if ( socket->state() != KTcpSocket::ConnectedState ) { 00159 // we have been disconnected by the error condition already, so just signal error result 00160 emit q->result( q ); 00161 q->deleteLater(); 00162 } 00163 } 00164 00165 bool sendCommandLine( const QByteArray &cmdline ) 00166 { 00167 if ( cmdline.length() < 4096 ) { 00168 kDebug(7112) << "C: >>" << cmdline.trimmed().data() << "<<"; 00169 } else { 00170 kDebug(7112) << "C: <" << cmdline.length() << " bytes>"; 00171 } 00172 ssize_t numWritten, cmdline_len = cmdline.length(); 00173 if ( ( numWritten = socket->write( cmdline ) ) != cmdline_len ) { 00174 kDebug(7112) << "Tried to write " << cmdline_len << " bytes, but only " 00175 << numWritten << " were written!" << endl; 00176 error( KIO::ERR_SLAVE_DEFINED, i18n ( "Writing to socket failed." ) ); 00177 return false; 00178 } 00179 return true; 00180 } 00181 00182 bool run( int type, TransactionState * ts = 0 ) 00183 { 00184 return run( Command::createSimpleCommand( type, this ), ts ); 00185 } 00186 00187 bool run( Command *cmd, TransactionState *ts = 0 ) 00188 { 00189 Q_ASSERT( cmd ); 00190 Q_ASSERT( !currentCommand ); 00191 Q_ASSERT( !currentTransactionState || currentTransactionState == ts ); 00192 00193 // ### WTF? 00194 if ( cmd->doNotExecute( ts ) ) { 00195 return true; 00196 } 00197 00198 currentCommand = cmd; 00199 currentTransactionState = ts; 00200 00201 while ( !cmd->isComplete() && !cmd->needsResponse() ) { 00202 const QByteArray cmdLine = cmd->nextCommandLine( ts ); 00203 if ( ts && ts->failedFatally() ) { 00204 q->disconnectFromHost( false ); 00205 return false; 00206 } 00207 if ( cmdLine.isEmpty() ) { 00208 continue; 00209 } 00210 if ( !sendCommandLine( cmdLine ) ) { 00211 q->disconnectFromHost( false ); 00212 return false; 00213 } 00214 } 00215 return true; 00216 } 00217 00218 void queueCommand( int type ) 00219 { 00220 queueCommand( Command::createSimpleCommand( type, this ) ); 00221 } 00222 00223 void queueCommand( KioSMTP::Command * command ) 00224 { 00225 mPendingCommandQueue.enqueue( command ); 00226 } 00227 00228 bool runQueuedCommands( TransactionState *ts ) 00229 { 00230 Q_ASSERT( ts ); 00231 Q_ASSERT( !currentTransactionState || ts == currentTransactionState ); 00232 currentTransactionState = ts; 00233 kDebug( canPipelineCommands(), 7112 ) << "using pipelining"; 00234 00235 while ( !mPendingCommandQueue.isEmpty() ) { 00236 QByteArray cmdline = collectPipelineCommands( ts ); 00237 if ( ts->failedFatally() ) { 00238 q->disconnectFromHost( false ); 00239 return false; 00240 } 00241 if ( ts->failed() ) { 00242 break; 00243 } 00244 if ( cmdline.isEmpty() ) { 00245 continue; 00246 } 00247 if ( !sendCommandLine( cmdline ) || ts->failedFatally() ) { 00248 q->disconnectFromHost( false ); 00249 return false; 00250 } 00251 if ( !mSentCommandQueue.isEmpty() ) { 00252 return true; // wait for responses 00253 } 00254 } 00255 00256 if ( ts->failed() ) { 00257 kDebug() << "transaction state failed: " << ts->errorCode() << ts->errorMessage(); 00258 if ( errorMessage.isEmpty() ) { 00259 errorMessage = ts->errorMessage(); 00260 } 00261 state = SmtpSessionPrivate::Reset; 00262 if ( !run( Command::RSET, currentTransactionState ) ) { 00263 q->disconnectFromHost( false ); 00264 } 00265 return false; 00266 } 00267 00268 delete currentTransactionState; 00269 currentTransactionState = 0; 00270 return true; 00271 } 00272 00273 QByteArray collectPipelineCommands( TransactionState *ts ) 00274 { 00275 Q_ASSERT( ts ); 00276 QByteArray cmdLine; 00277 unsigned int cmdLine_len = 0; 00278 00279 while ( !mPendingCommandQueue.isEmpty() ) { 00280 00281 Command * cmd = mPendingCommandQueue.head(); 00282 00283 if ( cmd->doNotExecute( ts ) ) { 00284 delete mPendingCommandQueue.dequeue(); 00285 if ( cmdLine_len ) { 00286 break; 00287 } else { 00288 continue; 00289 } 00290 } 00291 00292 if ( cmdLine_len && cmd->mustBeFirstInPipeline() ) { 00293 break; 00294 } 00295 00296 if ( cmdLine_len && !canPipelineCommands() ) { 00297 break; 00298 } 00299 00300 while ( !cmd->isComplete() && !cmd->needsResponse() ) { 00301 const QByteArray currentCmdLine = cmd->nextCommandLine( ts ); 00302 if ( ts->failedFatally() ) { 00303 return cmdLine; 00304 } 00305 const unsigned int currentCmdLine_len = currentCmdLine.length(); 00306 00307 cmdLine_len += currentCmdLine_len; 00308 cmdLine += currentCmdLine; 00309 00310 // If we are executing the transfer command, don't collect the whole 00311 // command line (which may be several MBs) before sending it, but instead 00312 // send the data each time we have collected 32 KB of the command line. 00313 // 00314 // This way, the progress information in clients like KMail works correctly, 00315 // because otherwise, the TransferCommand would read the whole data from the 00316 // job at once, then sending it. The progress update on the client however 00317 // happens when sending data to the job, not when this slave writes the data 00318 // to the socket. Therefore that progress update is incorrect. 00319 // 00320 // 32 KB seems to be a sensible limit. Additionally, a job can only transfer 00321 // 32 KB at once anyway. 00322 if ( dynamic_cast<TransferCommand *>( cmd ) != 0 && 00323 cmdLine_len >= 32 * 1024 ) { 00324 return cmdLine; 00325 } 00326 } 00327 00328 mSentCommandQueue.enqueue( mPendingCommandQueue.dequeue() ); 00329 00330 if ( cmd->mustBeLastInPipeline() ) { 00331 break; 00332 } 00333 } 00334 00335 return cmdLine; 00336 } 00337 00338 void receivedNewData() 00339 { 00340 kDebug(); 00341 while ( socket->canReadLine() ) { 00342 const QByteArray buffer = socket->readLine(); 00343 kDebug() << "S: >>" << buffer << "<<"; 00344 currentResponse.parseLine( buffer, buffer.size() ); 00345 // ...until the response is complete or the parser is so confused 00346 // that it doesn't think a RSET would help anymore: 00347 if ( currentResponse.isComplete() ) { 00348 handleResponse( currentResponse ); 00349 currentResponse = Response(); 00350 } else if ( !currentResponse.isWellFormed() ) { 00351 error( KIO::ERR_NO_CONTENT, 00352 i18n( "Invalid SMTP response (%1) received.", currentResponse.code() ) ); 00353 } 00354 } 00355 } 00356 00357 void handleResponse( const KioSMTP::Response &response ) 00358 { 00359 if ( !mSentCommandQueue.isEmpty() ) { 00360 Command *cmd = mSentCommandQueue.head(); 00361 Q_ASSERT( cmd->isComplete() ); 00362 cmd->processResponse( response, currentTransactionState ); 00363 if ( currentTransactionState->failedFatally() ) { 00364 q->disconnectFromHost( false ); 00365 } 00366 delete mSentCommandQueue.dequeue(); 00367 00368 if ( mSentCommandQueue.isEmpty() ) { 00369 if ( !mPendingCommandQueue.isEmpty() ) { 00370 runQueuedCommands( currentTransactionState ); 00371 } else if ( state == Sending ) { 00372 delete currentTransactionState; 00373 currentTransactionState = 0; 00374 q->disconnectFromHost(); // we are done 00375 } 00376 } 00377 return; 00378 } 00379 00380 if ( currentCommand ) { 00381 if ( !currentCommand->processResponse( response, currentTransactionState ) ) { 00382 q->disconnectFromHost( false ); 00383 } 00384 while ( !currentCommand->isComplete() && !currentCommand->needsResponse() ) { 00385 const QByteArray cmdLine = currentCommand->nextCommandLine( currentTransactionState ); 00386 if ( currentTransactionState && currentTransactionState->failedFatally() ) { 00387 q->disconnectFromHost( false ); 00388 } 00389 if ( cmdLine.isEmpty() ) { 00390 continue; 00391 } 00392 if ( !sendCommandLine( cmdLine ) ) { 00393 q->disconnectFromHost( false ); 00394 } 00395 } 00396 if ( currentCommand->isComplete() ) { 00397 Command *cmd = currentCommand; 00398 currentCommand = 0; 00399 currentTransactionState = 0; 00400 handleCommand( cmd ); 00401 } 00402 return; 00403 } 00404 00405 // command-less responses 00406 switch ( state ) { 00407 case Initial: // server greeting 00408 { 00409 if ( !response.isOk() ) { 00410 error( KIO::ERR_COULD_NOT_LOGIN, 00411 i18n( "The server (%1) did not accept the connection.\n%2", 00412 destination.host(), response.errorMessage() ) ); 00413 break; 00414 } 00415 state = EHLOPreTls; 00416 EHLOCommand *ehloCmdPreTLS = new EHLOCommand( this, myHostname ); 00417 run( ehloCmdPreTLS ); 00418 break; 00419 } 00420 default: error( KIO::ERR_SLAVE_DEFINED, i18n( "Unhandled response" ) ); 00421 } 00422 } 00423 00424 void handleCommand( Command *cmd ) 00425 { 00426 switch ( state ) { 00427 case StartTLS: 00428 { 00429 // re-issue EHLO to refresh the capability list (could be have 00430 // been faked before TLS was enabled): 00431 state = EHLOPostTls; 00432 EHLOCommand *ehloCmdPostTLS = new EHLOCommand( this, myHostname ); 00433 run( ehloCmdPostTLS ); 00434 break; 00435 } 00436 case EHLOPreTls: 00437 { 00438 if ( ( haveCapability( "STARTTLS" ) && 00439 tlsRequested() != SMTPSessionInterface::ForceNoTLS ) || 00440 tlsRequested() == SMTPSessionInterface::ForceTLS ) 00441 { 00442 state = StartTLS; 00443 run( Command::STARTTLS ); 00444 break; 00445 } 00446 } 00447 // fall through 00448 case EHLOPostTls: 00449 { 00450 // return with success if the server doesn't support SMTP-AUTH or an user 00451 // name is not specified and metadata doesn't tell us to force it. 00452 if ( !destination.user().isEmpty() || 00453 haveCapability( "AUTH" ) || 00454 !requestedSaslMethod().isEmpty() ) { 00455 authInfo.username = destination.user(); 00456 authInfo.password = destination.password(); 00457 authInfo.prompt = i18n( "Username and password for your SMTP account:" ); 00458 00459 QStringList strList; 00460 if ( !requestedSaslMethod().isEmpty() ) { 00461 strList.append( requestedSaslMethod() ); 00462 } else { 00463 strList = capabilities().saslMethodsQSL(); 00464 } 00465 00466 state = Authenticated; 00467 AuthCommand *authCmd = 00468 new AuthCommand( this, strList.join( QLatin1String( " " ) ).toLatin1(), 00469 destination.host(), authInfo ); 00470 run( authCmd ); 00471 break; 00472 } 00473 } 00474 // fall through 00475 case Authenticated: 00476 { 00477 state = Sending; 00478 queueCommand( new MailFromCommand( this, request.fromAddress().toLatin1(), 00479 request.is8BitBody(), request.size() ) ); 00480 // Loop through our To and CC recipients, and send the proper 00481 // SMTP commands, for the benefit of the server. 00482 const QStringList recipients = request.recipients(); 00483 for ( QStringList::const_iterator it = recipients.begin(); it != recipients.end(); ++it ) { 00484 queueCommand( new RcptToCommand( this, (*it).toLatin1() ) ); 00485 } 00486 00487 queueCommand( Command::DATA ); 00488 queueCommand( new TransferCommand( this, QByteArray() ) ); 00489 00490 TransactionState *ts = new TransactionState; 00491 if ( !runQueuedCommands( ts ) ) { 00492 if ( ts->errorCode() ) { 00493 error( ts->errorCode(), ts->errorMessage() ); 00494 } 00495 } 00496 break; 00497 } 00498 case Reset: 00499 q->disconnectFromHost( true ); 00500 break; 00501 default: 00502 error( KIO::ERR_SLAVE_DEFINED, i18n( "Unhandled command response." ) ); 00503 } 00504 00505 delete cmd; 00506 } 00507 00508 public: 00509 QString saslMethod; 00510 bool useTLS; 00511 00512 KUrl destination; 00513 KTcpSocket *socket; 00514 QIODevice *data; 00515 KioSMTP::Response currentResponse; 00516 KioSMTP::Command * currentCommand; 00517 KioSMTP::TransactionState *currentTransactionState; 00518 KIO::AuthInfo authInfo; 00519 KioSMTP::Request request; 00520 QString errorMessage; 00521 QString myHostname; 00522 00523 enum State { 00524 Initial, 00525 EHLOPreTls, 00526 StartTLS, 00527 EHLOPostTls, 00528 Authenticated, 00529 Sending, 00530 Reset 00531 }; 00532 State state; 00533 00534 typedef QQueue<KioSMTP::Command*> CommandQueue; 00535 CommandQueue mPendingCommandQueue; 00536 CommandQueue mSentCommandQueue; 00537 00538 static bool saslInitialized; 00539 00540 private: 00541 SmtpSession *q; 00542 }; 00543 00544 bool SmtpSessionPrivate::saslInitialized = false; 00545 00546 SmtpSession::SmtpSession( QObject *parent ) : 00547 QObject(parent), 00548 d( new SmtpSessionPrivate( this ) ) 00549 { 00550 kDebug(); 00551 d->socket = new KTcpSocket( this ); 00552 connect( d->socket, SIGNAL(connected()), SLOT(socketConnected()) ); 00553 connect( d->socket, SIGNAL(disconnected()), SLOT(socketDisconnected()) ); 00554 connect( d->socket, SIGNAL(error(KTcpSocket::Error)), SLOT(socketError(KTcpSocket::Error)) ); 00555 connect( d->socket, SIGNAL(readyRead()), SLOT(receivedNewData()), Qt::QueuedConnection ); 00556 00557 // hold connection for the lifetime of this session 00558 KPIMUtils::NetworkAccessHelper *networkHelper = new KPIMUtils::NetworkAccessHelper( this ); 00559 networkHelper->establishConnection(); 00560 00561 if ( !d->saslInitialized ) { 00562 if ( !initSASL() ) { 00563 exit( -1 ); 00564 } 00565 d->saslInitialized = true; 00566 } 00567 } 00568 00569 SmtpSession::~SmtpSession() 00570 { 00571 kDebug(); 00572 delete d; 00573 } 00574 00575 void SmtpSession::setSaslMethod( const QString &method ) 00576 { 00577 d->saslMethod = method; 00578 } 00579 00580 void SmtpSession::setUseTLS( bool useTLS ) 00581 { 00582 d->useTLS = useTLS; 00583 } 00584 00585 void SmtpSession::connectToHost( const KUrl &url ) 00586 { 00587 kDebug() << url; 00588 d->socket->connectToHost( url.host(), url.port() ); 00589 } 00590 00591 void SmtpSession::disconnectFromHost( bool nice ) 00592 { 00593 if ( d->socket->state() == KTcpSocket::ConnectedState ) { 00594 if ( nice ) { 00595 d->run( Command::QUIT ); 00596 } 00597 00598 d->socket->disconnectFromHost(); 00599 00600 d->clearCapabilities(); 00601 qDeleteAll( d->mPendingCommandQueue ); 00602 d->mPendingCommandQueue.clear(); 00603 qDeleteAll( d->mSentCommandQueue ); 00604 d->mSentCommandQueue.clear(); 00605 } 00606 } 00607 00608 void SmtpSession::sendMessage( const KUrl &destination, QIODevice *data ) 00609 { 00610 d->destination = destination; 00611 if ( d->socket->state() != KTcpSocket::ConnectedState && 00612 d->socket->state() != KTcpSocket::ConnectingState ) { 00613 connectToHost( destination ); 00614 } 00615 00616 d->data = data; 00617 d->request = Request::fromURL( destination ); // parse settings from URL's query 00618 00619 if ( !d->request.heloHostname().isEmpty() ) { 00620 d->myHostname = d->request.heloHostname(); 00621 } else { 00622 d->myHostname = QHostInfo::localHostName(); 00623 if( d->myHostname.isEmpty() ) { 00624 d->myHostname = QLatin1String( "localhost.invalid" ); 00625 } else if ( !d->myHostname.contains( QLatin1Char( '.' ) ) ) { 00626 d->myHostname += QLatin1String( ".localnet" ); 00627 } 00628 } 00629 } 00630 00631 QString SmtpSession::errorMessage() const 00632 { 00633 return d->errorMessage; 00634 } 00635 00636 #include "smtpsession.moc"