• Skip to content
  • Skip to link menu
KDE 4.6 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 
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"

mailtransport

Skip menu "mailtransport"
  • Main Page
  • Class Hierarchy
  • Alphabetical List
  • Class List
  • File List
  • Class Members
  • 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.3
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