• Skip to content
  • Skip to link menu
  • KDE API Reference
  • kdepimlibs-4.8.5 API Reference
  • 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       }
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"
This file is part of the KDE documentation.
Documentation copyright © 1996-2012 The KDE developers.
Generated on Thu Aug 2 2012 15:25:02 by doxygen 1.7.5 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.

mailtransport

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

kdepimlibs-4.8.5 API Reference

Skip menu "kdepimlibs-4.8.5 API Reference"
  • akonadi
  •   contact
  •   kmime
  • kabc
  • kalarmcal
  • 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
Report problems with this website to our bug tracking system.
Contact the specific authors with questions and comments about the page contents.

KDE® and the K Desktop Environment® logo are registered trademarks of KDE e.V. | Legal