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