• Skip to content
  • Skip to link menu
  • KDE API Reference
  • kdepimlibs-4.10.5 API Reference
  • KDE Home
  • Contact Us
 

mailtransport

  • mailtransport
  • smtp
smtpsession.cpp
1 /*
2  Copyright (c) 2010 Volker Krause <vkrause@kde.org>
3 
4  This library is free software; you can redistribute it and/or modify it
5  under the terms of the GNU Library General Public License as published by
6  the Free Software Foundation; either version 2 of the License, or (at your
7  option) any later version.
8 
9  This library is distributed in the hope that it will be useful, but WITHOUT
10  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11  FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public
12  License for more details.
13 
14  You should have received a copy of the GNU Library General Public License
15  along with this library; see the file COPYING.LIB. If not, write to the
16  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17  02110-1301, USA.
18 */
19 
20 #include "smtpsession.h"
21 
22 #include "common.h"
23 #include "smtp/smtpsessioninterface.h"
24 #include "smtp/request.h"
25 #include "smtp/response.h"
26 #include "smtp/command.h"
27 #include "smtp/transactionstate.h"
28 
29 #include <ktcpsocket.h>
30 #include <KMessageBox>
31 #include <KIO/PasswordDialog>
32 #include <kio/authinfo.h>
33 #include <kio/global.h>
34 #include <kio/sslui.h>
35 #include <KLocalizedString>
36 #include <kpimutils/networkaccesshelper.h>
37 #include <KDebug>
38 
39 #include <QtCore/QQueue>
40 #include <QtNetwork/QHostInfo>
41 
42 using namespace MailTransport;
43 using namespace KioSMTP;
44 
45 class MailTransport::SmtpSessionPrivate : public KioSMTP::SMTPSessionInterface
46 {
47  public:
48  explicit SmtpSessionPrivate( SmtpSession *session ) :
49  useTLS( true ),
50  socket( 0 ),
51  currentCommand( 0 ),
52  currentTransactionState( 0 ),
53  state( Initial ),
54  q( session )
55  {}
56 
57  void dataReq() { /* noop */ };
58  int readData( QByteArray &ba )
59  {
60  if ( data->atEnd() ) {
61  ba.clear();
62  return 0;
63  } else {
64  Q_ASSERT( data->isOpen() );
65  ba = data->read( 32 * 1024 );
66  return ba.size();
67  }
68  }
69 
70  void error( int id, const QString &msg )
71  {
72  kDebug() << id << msg;
73  // clear state so further replies don't end up in failed commands etc.
74  currentCommand = 0;
75  currentTransactionState = 0;
76 
77  if ( errorMessage.isEmpty() ) {
78  errorMessage = KIO::buildErrorString( id, msg );
79  }
80  q->disconnectFromHost();
81  }
82 
83  void informationMessageBox( const QString &msg, const QString &caption )
84  {
85  KMessageBox::information( 0, msg, caption );
86  }
87 
88  bool openPasswordDialog( KIO::AuthInfo &authInfo ) {
89  return KIO::PasswordDialog::getNameAndPassword(
90  authInfo.username,
91  authInfo.password,
92  &( authInfo.keepPassword ),
93  authInfo.prompt,
94  authInfo.readOnly,
95  authInfo.caption,
96  authInfo.comment,
97  authInfo.commentLabel ) == KIO::PasswordDialog::Accepted;
98  }
99 
100  bool startSsl()
101  {
102  kDebug();
103  Q_ASSERT( socket );
104  socket->setAdvertisedSslVersion( KTcpSocket::TlsV1 );
105  socket->ignoreSslErrors();
106  socket->startClientEncryption();
107  const bool encrypted = socket->waitForEncrypted( 60 * 1000 );
108 
109  const KSslCipher cipher = socket->sessionCipher();
110  if ( !encrypted ||
111  socket->sslErrors().count() > 0 ||
112  socket->encryptionMode() != KTcpSocket::SslClientMode ||
113  cipher.isNull() ||
114  cipher.usedBits() == 0 ) {
115  kDebug() << "Initial SSL handshake failed. cipher.isNull() is" << cipher.isNull()
116  << ", cipher.usedBits() is" << cipher.usedBits()
117  << ", the socket says:" << socket->errorString()
118  << "and the list of SSL errors contains"
119  << socket->sslErrors().count() << "items.";
120 
121  if ( KIO::SslUi::askIgnoreSslErrors( socket ) ) {
122  return true;
123  } else {
124  return false;
125  }
126  } else {
127  kDebug() << "TLS negotiation done.";
128  return true;
129  }
130  }
131 
132  bool lf2crlfAndDotStuffingRequested() const { return true; }
133  QString requestedSaslMethod() const { return saslMethod; }
134  TLSRequestState tlsRequested() const { return useTLS ? ForceTLS : ForceNoTLS; }
135 
136  void socketConnected()
137  {
138  kDebug();
139  if ( destination.protocol() == QLatin1String( "smtps" ) ) {
140  if ( !startSsl() ) {
141  error( KIO::ERR_SLAVE_DEFINED, i18n( "SSL negotiation failed." ) );
142  }
143  }
144  }
145 
146  void socketDisconnected()
147  {
148  kDebug();
149  emit q->result( q );
150  q->deleteLater();
151  }
152 
153  void socketError( KTcpSocket::Error err )
154  {
155  kDebug() << err;
156  error( KIO::ERR_CONNECTION_BROKEN, socket->errorString() );
157 
158  if ( socket->state() != KTcpSocket::ConnectedState ) {
159  // we have been disconnected by the error condition already, so just signal error result
160  emit q->result( q );
161  q->deleteLater();
162  }
163  }
164 
165  bool sendCommandLine( const QByteArray &cmdline )
166  {
167  if ( cmdline.length() < 4096 ) {
168  kDebug( 7112 ) << "C: >>" << cmdline.trimmed().data() << "<<";
169  } else {
170  kDebug( 7112 ) << "C: <" << cmdline.length() << " bytes>";
171  }
172  ssize_t numWritten, cmdline_len = cmdline.length();
173  if ( ( numWritten = socket->write( cmdline ) ) != cmdline_len ) {
174  kDebug( 7112 ) << "Tried to write " << cmdline_len << " bytes, but only "
175  << numWritten << " were written!" << endl;
176  error( KIO::ERR_SLAVE_DEFINED, i18n ( "Writing to socket failed." ) );
177  return false;
178  }
179  return true;
180  }
181 
182  bool run( int type, TransactionState * ts = 0 )
183  {
184  return run( Command::createSimpleCommand( type, this ), ts );
185  }
186 
187  bool run( Command *cmd, TransactionState *ts = 0 )
188  {
189  Q_ASSERT( cmd );
190  Q_ASSERT( !currentCommand );
191  Q_ASSERT( !currentTransactionState || currentTransactionState == ts );
192 
193  // ### WTF?
194  if ( cmd->doNotExecute( ts ) ) {
195  return true;
196  }
197 
198  currentCommand = cmd;
199  currentTransactionState = ts;
200 
201  while ( !cmd->isComplete() && !cmd->needsResponse() ) {
202  const QByteArray cmdLine = cmd->nextCommandLine( ts );
203  if ( ts && ts->failedFatally() ) {
204  q->disconnectFromHost( false );
205  return false;
206  }
207  if ( cmdLine.isEmpty() ) {
208  continue;
209  }
210  if ( !sendCommandLine( cmdLine ) ) {
211  q->disconnectFromHost( false );
212  return false;
213  }
214  }
215  return true;
216  }
217 
218  void queueCommand( int type )
219  {
220  queueCommand( Command::createSimpleCommand( type, this ) );
221  }
222 
223  void queueCommand( KioSMTP::Command * command )
224  {
225  mPendingCommandQueue.enqueue( command );
226  }
227 
228  bool runQueuedCommands( TransactionState *ts )
229  {
230  Q_ASSERT( ts );
231  Q_ASSERT( !currentTransactionState || ts == currentTransactionState );
232  currentTransactionState = ts;
233  kDebug( canPipelineCommands(), 7112 ) << "using pipelining";
234 
235  while ( !mPendingCommandQueue.isEmpty() ) {
236  QByteArray cmdline = collectPipelineCommands( ts );
237  if ( ts->failedFatally() ) {
238  q->disconnectFromHost( false );
239  return false;
240  }
241  if ( ts->failed() ) {
242  break;
243  }
244  if ( cmdline.isEmpty() ) {
245  continue;
246  }
247  if ( !sendCommandLine( cmdline ) || ts->failedFatally() ) {
248  q->disconnectFromHost( false );
249  return false;
250  }
251  if ( !mSentCommandQueue.isEmpty() ) {
252  return true; // wait for responses
253  }
254  }
255 
256  if ( ts->failed() ) {
257  kDebug() << "transaction state failed: " << ts->errorCode() << ts->errorMessage();
258  if ( errorMessage.isEmpty() ) {
259  errorMessage = ts->errorMessage();
260  }
261  state = SmtpSessionPrivate::Reset;
262  if ( !run( Command::RSET, currentTransactionState ) ) {
263  q->disconnectFromHost( false );
264  }
265  return false;
266  }
267 
268  delete currentTransactionState;
269  currentTransactionState = 0;
270  return true;
271  }
272 
273  QByteArray collectPipelineCommands( TransactionState *ts )
274  {
275  Q_ASSERT( ts );
276  QByteArray cmdLine;
277  unsigned int cmdLine_len = 0;
278 
279  while ( !mPendingCommandQueue.isEmpty() ) {
280 
281  Command * cmd = mPendingCommandQueue.head();
282 
283  if ( cmd->doNotExecute( ts ) ) {
284  delete mPendingCommandQueue.dequeue();
285  if ( cmdLine_len ) {
286  break;
287  } else {
288  continue;
289  }
290  }
291 
292  if ( cmdLine_len && cmd->mustBeFirstInPipeline() ) {
293  break;
294  }
295 
296  if ( cmdLine_len && !canPipelineCommands() ) {
297  break;
298  }
299 
300  while ( !cmd->isComplete() && !cmd->needsResponse() ) {
301  const QByteArray currentCmdLine = cmd->nextCommandLine( ts );
302  if ( ts->failedFatally() ) {
303  return cmdLine;
304  }
305  const unsigned int currentCmdLine_len = currentCmdLine.length();
306 
307  cmdLine_len += currentCmdLine_len;
308  cmdLine += currentCmdLine;
309 
310  // If we are executing the transfer command, don't collect the whole
311  // command line (which may be several MBs) before sending it, but instead
312  // send the data each time we have collected 32 KB of the command line.
313  //
314  // This way, the progress information in clients like KMail works correctly,
315  // because otherwise, the TransferCommand would read the whole data from the
316  // job at once, then sending it. The progress update on the client however
317  // happens when sending data to the job, not when this slave writes the data
318  // to the socket. Therefore that progress update is incorrect.
319  //
320  // 32 KB seems to be a sensible limit. Additionally, a job can only transfer
321  // 32 KB at once anyway.
322  if ( dynamic_cast<TransferCommand *>( cmd ) != 0 &&
323  cmdLine_len >= 32 * 1024 ) {
324  return cmdLine;
325  }
326  }
327 
328  mSentCommandQueue.enqueue( mPendingCommandQueue.dequeue() );
329 
330  if ( cmd->mustBeLastInPipeline() ) {
331  break;
332  }
333  }
334 
335  return cmdLine;
336  }
337 
338  void receivedNewData()
339  {
340  kDebug();
341  while ( socket->canReadLine() ) {
342  const QByteArray buffer = socket->readLine();
343  kDebug() << "S: >>" << buffer << "<<";
344  currentResponse.parseLine( buffer, buffer.size() );
345  // ...until the response is complete or the parser is so confused
346  // that it doesn't think a RSET would help anymore:
347  if ( currentResponse.isComplete() ) {
348  handleResponse( currentResponse );
349  currentResponse = Response();
350  } else if ( !currentResponse.isWellFormed() ) {
351  error( KIO::ERR_NO_CONTENT,
352  i18n( "Invalid SMTP response (%1) received.", currentResponse.code() ) );
353  }
354  }
355  }
356 
357  void handleResponse( const KioSMTP::Response &response )
358  {
359  if ( !mSentCommandQueue.isEmpty() ) {
360  Command *cmd = mSentCommandQueue.head();
361  Q_ASSERT( cmd->isComplete() );
362  cmd->processResponse( response, currentTransactionState );
363  if ( currentTransactionState->failedFatally() ) {
364  q->disconnectFromHost( false );
365  }
366  delete mSentCommandQueue.dequeue();
367 
368  if ( mSentCommandQueue.isEmpty() ) {
369  if ( !mPendingCommandQueue.isEmpty() ) {
370  runQueuedCommands( currentTransactionState );
371  } else if ( state == Sending ) {
372  delete currentTransactionState;
373  currentTransactionState = 0;
374  q->disconnectFromHost(); // we are done
375  }
376  }
377  return;
378  }
379 
380  if ( currentCommand ) {
381  if ( !currentCommand->processResponse( response, currentTransactionState ) ) {
382  q->disconnectFromHost( false );
383  }
384  while ( !currentCommand->isComplete() && !currentCommand->needsResponse() ) {
385  const QByteArray cmdLine = currentCommand->nextCommandLine( currentTransactionState );
386  if ( currentTransactionState && currentTransactionState->failedFatally() ) {
387  q->disconnectFromHost( false );
388  }
389  if ( cmdLine.isEmpty() ) {
390  continue;
391  }
392  if ( !sendCommandLine( cmdLine ) ) {
393  q->disconnectFromHost( false );
394  }
395  }
396  if ( currentCommand->isComplete() ) {
397  Command *cmd = currentCommand;
398  currentCommand = 0;
399  currentTransactionState = 0;
400  handleCommand( cmd );
401  }
402  return;
403  }
404 
405  // command-less responses
406  switch ( state ) {
407  case Initial: // server greeting
408  {
409  if ( !response.isOk() ) {
410  error( KIO::ERR_COULD_NOT_LOGIN,
411  i18n( "The server (%1) did not accept the connection.\n%2",
412  destination.host(), response.errorMessage() ) );
413  break;
414  }
415  state = EHLOPreTls;
416  EHLOCommand *ehloCmdPreTLS = new EHLOCommand( this, myHostname );
417  run( ehloCmdPreTLS );
418  break;
419  }
420  default: error( KIO::ERR_SLAVE_DEFINED, i18n( "Unhandled response" ) );
421  }
422  }
423 
424  void handleCommand( Command *cmd )
425  {
426  switch ( state ) {
427  case StartTLS:
428  {
429  // re-issue EHLO to refresh the capability list (could be have
430  // been faked before TLS was enabled):
431  state = EHLOPostTls;
432  EHLOCommand *ehloCmdPostTLS = new EHLOCommand( this, myHostname );
433  run( ehloCmdPostTLS );
434  break;
435  }
436  case EHLOPreTls:
437  {
438  if ( ( haveCapability( "STARTTLS" ) &&
439  tlsRequested() != SMTPSessionInterface::ForceNoTLS ) ||
440  tlsRequested() == SMTPSessionInterface::ForceTLS )
441  {
442  state = StartTLS;
443  run( Command::STARTTLS );
444  break;
445  }
446  }
447  // fall through
448  case EHLOPostTls:
449  {
450  // return with success if the server doesn't support SMTP-AUTH or an user
451  // name is not specified and metadata doesn't tell us to force it.
452  if ( !destination.user().isEmpty() ||
453  haveCapability( "AUTH" ) ||
454  !requestedSaslMethod().isEmpty() ) {
455  authInfo.username = destination.user();
456  authInfo.password = destination.password();
457  authInfo.prompt = i18n( "Username and password for your SMTP account:" );
458 
459  QStringList strList;
460  if ( !requestedSaslMethod().isEmpty() ) {
461  strList.append( requestedSaslMethod() );
462  } else {
463  strList = capabilities().saslMethodsQSL();
464  }
465 
466  state = Authenticated;
467  AuthCommand *authCmd =
468  new AuthCommand( this, strList.join( QLatin1String( " " ) ).toLatin1(),
469  destination.host(), authInfo );
470  run( authCmd );
471  break;
472  }
473  }
474  // fall through
475  case Authenticated:
476  {
477  state = Sending;
478  queueCommand( new MailFromCommand( this, request.fromAddress().toLatin1(),
479  request.is8BitBody(), request.size() ) );
480  // Loop through our To and CC recipients, and send the proper
481  // SMTP commands, for the benefit of the server.
482  const QStringList recipients = request.recipients();
483  for ( QStringList::const_iterator it = recipients.begin(); it != recipients.end(); ++it ) {
484  queueCommand( new RcptToCommand( this, ( *it ).toLatin1() ) );
485  }
486 
487  queueCommand( Command::DATA );
488  queueCommand( new TransferCommand( this, QByteArray() ) );
489 
490  TransactionState *ts = new TransactionState;
491  if ( !runQueuedCommands( ts ) ) {
492  if ( ts->errorCode() ) {
493  error( ts->errorCode(), ts->errorMessage() );
494  }
495  }
496  break;
497  }
498  case Reset:
499  q->disconnectFromHost( true );
500  break;
501  default:
502  error( KIO::ERR_SLAVE_DEFINED, i18n( "Unhandled command response." ) );
503  }
504 
505  delete cmd;
506  }
507 
508  public:
509  QString saslMethod;
510  bool useTLS;
511 
512  KUrl destination;
513  KTcpSocket *socket;
514  QIODevice *data;
515  KioSMTP::Response currentResponse;
516  KioSMTP::Command * currentCommand;
517  KioSMTP::TransactionState *currentTransactionState;
518  KIO::AuthInfo authInfo;
519  KioSMTP::Request request;
520  QString errorMessage;
521  QString myHostname;
522 
523  enum State {
524  Initial,
525  EHLOPreTls,
526  StartTLS,
527  EHLOPostTls,
528  Authenticated,
529  Sending,
530  Reset
531  };
532  State state;
533 
534  typedef QQueue<KioSMTP::Command*> CommandQueue;
535  CommandQueue mPendingCommandQueue;
536  CommandQueue mSentCommandQueue;
537 
538  static bool saslInitialized;
539 
540  private:
541  SmtpSession *q;
542 };
543 
544 bool SmtpSessionPrivate::saslInitialized = false;
545 
546 SmtpSession::SmtpSession( QObject *parent ) :
547  QObject( parent ),
548  d( new SmtpSessionPrivate( this ) )
549 {
550  kDebug();
551  d->socket = new KTcpSocket( this );
552  connect( d->socket, SIGNAL(connected()), SLOT(socketConnected()) );
553  connect( d->socket, SIGNAL(disconnected()), SLOT(socketDisconnected()) );
554  connect( d->socket, SIGNAL(error(KTcpSocket::Error)), SLOT(socketError(KTcpSocket::Error)) );
555  connect( d->socket, SIGNAL(readyRead()), SLOT(receivedNewData()), Qt::QueuedConnection );
556 
557  // hold connection for the lifetime of this session
558  KPIMUtils::NetworkAccessHelper *networkHelper = new KPIMUtils::NetworkAccessHelper( this );
559  networkHelper->establishConnection();
560 
561  if ( !d->saslInitialized ) {
562  if ( !initSASL() ) {
563  exit( -1 );
564  }
565  d->saslInitialized = true;
566  }
567 }
568 
569 SmtpSession::~SmtpSession()
570 {
571  kDebug();
572  delete d;
573 }
574 
575 void SmtpSession::setSaslMethod( const QString &method )
576 {
577  d->saslMethod = method;
578 }
579 
580 void SmtpSession::setUseTLS( bool useTLS )
581 {
582  d->useTLS = useTLS;
583 }
584 
585 void SmtpSession::connectToHost( const KUrl &url )
586 {
587  kDebug() << url;
588  d->socket->connectToHost( url.host(), url.port() );
589 }
590 
591 void SmtpSession::disconnectFromHost( bool nice )
592 {
593  if ( d->socket->state() == KTcpSocket::ConnectedState ) {
594  if ( nice ) {
595  d->run( Command::QUIT );
596  }
597 
598  d->socket->disconnectFromHost();
599 
600  d->clearCapabilities();
601  qDeleteAll( d->mPendingCommandQueue );
602  d->mPendingCommandQueue.clear();
603  qDeleteAll( d->mSentCommandQueue );
604  d->mSentCommandQueue.clear();
605  }
606 }
607 
608 void SmtpSession::sendMessage( const KUrl &destination, QIODevice *data )
609 {
610  d->destination = destination;
611  if ( d->socket->state() != KTcpSocket::ConnectedState &&
612  d->socket->state() != KTcpSocket::ConnectingState ) {
613  connectToHost( destination );
614  }
615 
616  d->data = data;
617  d->request = Request::fromURL( destination ); // parse settings from URL's query
618 
619  if ( !d->request.heloHostname().isEmpty() ) {
620  d->myHostname = d->request.heloHostname();
621  } else {
622  d->myHostname = QHostInfo::localHostName();
623  if ( d->myHostname.isEmpty() ) {
624  d->myHostname = QLatin1String( "localhost.invalid" );
625  } else if ( !d->myHostname.contains( QLatin1Char( '.' ) ) ) {
626  d->myHostname += QLatin1String( ".localnet" );
627  }
628  }
629 }
630 
631 QString SmtpSession::errorMessage() const
632 {
633  return d->errorMessage;
634 }
635 
636 #include "moc_smtpsession.cpp"
This file is part of the KDE documentation.
Documentation copyright © 1996-2013 The KDE developers.
Generated on Sat Jul 13 2013 01:26:53 by doxygen 1.8.3.1 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.10.5 API Reference

Skip menu "kdepimlibs-4.10.5 API Reference"
  • akonadi
  •   contact
  •   kmime
  •   socialutils
  • kabc
  • kalarmcal
  • kblog
  • kcal
  • kcalcore
  • kcalutils
  • kholidays
  • kimap
  • kioslave
  •   imap4
  •   mbox
  •   nntp
  • kldap
  • kmbox
  • kmime
  • kontactinterface
  • kpimidentities
  • kpimtextedit
  • 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