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

mailtransport

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

Skip menu "kdepimlibs 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

Search



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