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