• Skip to content
  • Skip to link menu
KDE 4.7 API Reference
  • KDE API Reference
  • KDE-PIM Libraries
  • KDE Home
  • Contact Us
 

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"

mailtransport

Skip menu "mailtransport"
  • Main Page
  • Alphabetical List
  • Class List
  • Class Hierarchy
  • Class Members
  • File List
  • Related Pages

KDE-PIM Libraries

Skip menu "KDE-PIM Libraries"
  • akonadi
  •   contact
  •   kmime
  • kabc
  • kblog
  • kcal
  • kcalcore
  • kcalutils
  • kholidays
  • kimap
  • kioslave
  •   imap4
  •   mbox
  •   nntp
  • kldap
  • kmbox
  • kmime
  • kontactinterface
  • kpimidentities
  • kpimtextedit
  •   richtextbuilders
  • kpimutils
  • kresources
  • ktnef
  • kxmlrpcclient
  • mailtransport
  • microblog
  • qgpgme
  • syndication
  •   atom
  •   rdf
  •   rss2
Generated for KDE-PIM Libraries by doxygen 1.7.5
This website is maintained by Adriaan de Groot and Allen Winter.
KDE® and the K Desktop Environment® logo are registered trademarks of KDE e.V. | Legal