diff options
Diffstat (limited to 'kioslave/smtp')
-rw-r--r-- | kioslave/smtp/Makefile.am | 37 | ||||
-rw-r--r-- | kioslave/smtp/TODO | 11 | ||||
-rw-r--r-- | kioslave/smtp/capabilities.cc | 143 | ||||
-rw-r--r-- | kioslave/smtp/capabilities.h | 77 | ||||
-rw-r--r-- | kioslave/smtp/command.cc | 606 | ||||
-rw-r--r-- | kioslave/smtp/command.h | 283 | ||||
-rw-r--r-- | kioslave/smtp/compliance.txt | 33 | ||||
-rw-r--r-- | kioslave/smtp/interactivesmtpserver.cc | 127 | ||||
-rw-r--r-- | kioslave/smtp/interactivesmtpserver.h | 121 | ||||
-rw-r--r-- | kioslave/smtp/request.cc | 189 | ||||
-rw-r--r-- | kioslave/smtp/request.h | 100 | ||||
-rw-r--r-- | kioslave/smtp/response.cc | 160 | ||||
-rw-r--r-- | kioslave/smtp/response.h | 125 | ||||
-rw-r--r-- | kioslave/smtp/smtp.cc | 647 | ||||
-rw-r--r-- | kioslave/smtp/smtp.h | 146 | ||||
-rw-r--r-- | kioslave/smtp/smtp.protocol | 16 | ||||
-rw-r--r-- | kioslave/smtp/smtps.protocol | 15 | ||||
-rw-r--r-- | kioslave/smtp/test_commands.cc | 728 | ||||
-rw-r--r-- | kioslave/smtp/test_headergeneration.cc | 86 | ||||
-rw-r--r-- | kioslave/smtp/test_responseparser.cc | 107 | ||||
-rw-r--r-- | kioslave/smtp/transactionstate.cc | 114 | ||||
-rw-r--r-- | kioslave/smtp/transactionstate.h | 185 |
22 files changed, 4056 insertions, 0 deletions
diff --git a/kioslave/smtp/Makefile.am b/kioslave/smtp/Makefile.am new file mode 100644 index 000000000..4ebe8ddd8 --- /dev/null +++ b/kioslave/smtp/Makefile.am @@ -0,0 +1,37 @@ + +INCLUDES= -I$(srcdir)/../.. -I$(srcdir)/.. $(SSL_INCLUDES) $(all_includes) + +kde_module_LTLIBRARIES = kio_smtp.la + +kio_smtp_la_SOURCES = smtp.cc request.cc response.cc capabilities.cc command.cc transactionstate.cc +kio_smtp_la_LIBADD = $(LIB_KIO) $(SASL2_LIBS) +kio_smtp_la_LDFLAGS = $(all_libraries) -module $(KDE_PLUGIN) + +noinst_HEADERS = smtp.h request.h response.h capabilities.h command.h transactionstate.h + +kdelnk_DATA = smtp.protocol smtps.protocol +kdelnkdir = $(kde_servicesdir) + +TESTS = test_headergeneration test_responseparser test_commands + +check_PROGRAMS = $(TESTS) interactivesmtpserver + +test_headergeneration_SOURCES = test_headergeneration.cc +test_headergeneration_LDADD = $(LIB_KDECORE) +test_headergeneration_LDFLAGS = $(all_libraries) + +test_responseparser_SOURCES = test_responseparser.cc +test_responseparser_LDADD = $(LIB_KDECORE) +test_responseparser_LDFLAGS = $(all_libraries) + +test_commands_SOURCES = test_commands.cc +test_commands_LDADD = $(kio_smtp_la_LIBADD) +test_commands_LDFLAGS = $(all_libraries) + +interactivesmtpserver_SOURCES = interactivesmtpserver.cc +interactivesmtpserver_LDADD = $(LIB_QT) +interactivesmtpserver_LDFLAGS = $(all_libraries) +interactivesmtpserver_METASOURCES = AUTO + +messages: + $(XGETTEXT) *.cc -o $(podir)/kio_smtp.pot diff --git a/kioslave/smtp/TODO b/kioslave/smtp/TODO new file mode 100644 index 000000000..cad79f139 --- /dev/null +++ b/kioslave/smtp/TODO @@ -0,0 +1,11 @@ +1. Double check the error handling and review error message in various + failure modes. +2. Implement the CHUNKING extension (rfc 3030; as soon as I find an + SMTP server that supports it). +3. Better error message (translated standard meanings of the known + response codes, ENHANCEDSTATUSCODES extension (rfc2034)). +4. (KIO) MultiPutJob to support pipelining across messages. +5. Ged rid of slave's header generation after checking who on earth + uses that... + +and further refactoring to make the code pleasant to look at ;-) diff --git a/kioslave/smtp/capabilities.cc b/kioslave/smtp/capabilities.cc new file mode 100644 index 000000000..a26626ce1 --- /dev/null +++ b/kioslave/smtp/capabilities.cc @@ -0,0 +1,143 @@ +/* -*- c++ -*- + capabilities.cc + + This file is part of kio_smtp, the KDE SMTP kioslave. + Copyright (c) 2003 Marc Mutz <mutz@kde.org> + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License, version 2, as + published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + In addition, as a special exception, the copyright holders give + permission to link the code of this program with any edition of + the Qt library by Trolltech AS, Norway (or with modified versions + of Qt that use the same license as Qt), and distribute linked + combinations including the two. You must obey the GNU General + Public License in all respects for all of the code used other than + Qt. If you modify this file, you may extend this exception to + your version of the file, but you are not obligated to do so. If + you do not wish to do so, delete this exception statement from + your version. +*/ + +#include <config.h> + +#include "capabilities.h" + +#include "response.h" + +#include <qstrlist.h> + +namespace KioSMTP { + + Capabilities Capabilities::fromResponse( const Response & ehlo ) { + Capabilities c; + + // first, check whether the response was valid and indicates success: + if ( !ehlo.isOk() + || ehlo.code() / 10 != 25 // ### restrict to 250 only? + || ehlo.lines().empty() ) + return c; + + QCStringList l = ehlo.lines(); + + for ( QCStringList::const_iterator it = ++l.begin() ; it != l.end() ; ++it ) + c.add( *it ); + + return c; + } + + void Capabilities::add( const QString & cap, bool replace ) { + QStringList tokens = QStringList::split( ' ', cap.upper() ); + if ( tokens.empty() ) + return; + QString name = tokens.front(); tokens.pop_front(); + add( name, tokens, replace ); + } + + void Capabilities::add( const QString & name, const QStringList & args, bool replace ) { + if ( replace ) + mCapabilities[name] = args; + else + mCapabilities[name] += args; + } + + QString Capabilities::asMetaDataString() const { + QString result; + for ( QMap<QString,QStringList>::const_iterator it = mCapabilities.begin() ; it != mCapabilities.end() ; ++it ) { + result += it.key(); + if ( !it.data().empty() ) + result += ' ' + it.data().join( " " ); + result += '\n'; + } + return result; + } + + QString Capabilities::authMethodMetaData() const { + QStringList sl = saslMethodsQSL(); + QString result; + for ( QStringList::const_iterator it = sl.begin() ; it != sl.end() ; ++it ) + result += "SASL/" + *it + '\n'; + return result; + } + + QStrIList Capabilities::saslMethods() const { + QStrIList result( true ); // deep copies to be safe + QStringList sl = saslMethodsQSL(); + for ( QStringList::const_iterator it = sl.begin() ; it != sl.end() ; ++it ) + result.append( (*it).latin1() ); + return result; + } + + QString Capabilities::createSpecialResponse( bool tls ) const { + QStringList result; + if ( tls ) + result.push_back( "STARTTLS" ); + result += saslMethodsQSL(); + if ( have( "PIPELINING" ) ) + result.push_back( "PIPELINING" ); + if ( have( "8BITMIME" ) ) + result.push_back( "8BITMIME" ); + if ( have( "SIZE" ) ) { + bool ok = false; + unsigned int size = mCapabilities["SIZE"].front().toUInt( &ok ); + if ( ok && !size ) + result.push_back( "SIZE=*" ); // any size + else if ( ok ) + result.push_back( "SIZE=" + QString::number( size ) ); // fixed max + else + result.push_back( "SIZE" ); // indetermined + } + return result.join( " " ); + } + + QStringList Capabilities::saslMethodsQSL() const { + QStringList result; + for ( QMap<QString,QStringList>::const_iterator it = mCapabilities.begin() ; it != mCapabilities.end() ; ++it ) { + if ( it.key() == "AUTH" ) + result += it.data(); + else if ( it.key().startsWith( "AUTH=" ) ) { + result.push_back( it.key().mid( qstrlen("AUTH=") ) ); + result += it.data(); + } + } + result.sort(); + QStringList::iterator it = result.begin(); + for (QStringList::iterator ot = it++; it != result.end(); ot = it++) + if (*ot == *it) result.remove(ot); + return result; + } + + + +} // namespace KioSMTP + diff --git a/kioslave/smtp/capabilities.h b/kioslave/smtp/capabilities.h new file mode 100644 index 000000000..1ae5972aa --- /dev/null +++ b/kioslave/smtp/capabilities.h @@ -0,0 +1,77 @@ +/* -*- c++ -*- + capabilities.h + + This file is part of kio_smtp, the KDE SMTP kioslave. + Copyright (c) 2003 Marc Mutz <mutz@kde.org> + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License, version 2, as + published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + In addition, as a special exception, the copyright holders give + permission to link the code of this program with any edition of + the Qt library by Trolltech AS, Norway (or with modified versions + of Qt that use the same license as Qt), and distribute linked + combinations including the two. You must obey the GNU General + Public License in all respects for all of the code used other than + Qt. If you modify this file, you may extend this exception to + your version of the file, but you are not obligated to do so. If + you do not wish to do so, delete this exception statement from + your version. +*/ + +#ifndef __KIOSMTP_CAPABILITIES_H__ +#define __KIOSMTP_CAPABILITIES_H__ + +#include <qmap.h> +#include <qcstring.h> +#include <qstring.h> +#include <qstringlist.h> + +class QStrIList; + +namespace KioSMTP { + + class Response; + + class Capabilities { + public: + Capabilities() {} + + static Capabilities fromResponse( const Response & response ); + + void add( const QString & cap, bool replace=false ); + void add( const QString & name, const QStringList & args, bool replace=false ); + void clear() { mCapabilities.clear(); } + + bool have( const QString & cap ) const { + return mCapabilities.find( cap.upper() ) != mCapabilities.end(); + } + bool have( const QCString & cap ) const { return have( QString( cap.data() ) ); } + bool have( const char * cap ) const { return have( QString::fromLatin1( cap ) ); } + + QString asMetaDataString() const; + + QString authMethodMetaData() const; + QStrIList saslMethods() const; + + QString createSpecialResponse( bool tls ) const; + + QStringList saslMethodsQSL() const; + private: + + QMap<QString,QStringList> mCapabilities; + }; + +} // namespace KioSMTP + +#endif // __KIOSMTP_CAPABILITIES_H__ diff --git a/kioslave/smtp/command.cc b/kioslave/smtp/command.cc new file mode 100644 index 000000000..9fb7281c9 --- /dev/null +++ b/kioslave/smtp/command.cc @@ -0,0 +1,606 @@ +/* -*- c++ -*- + command.cc + + This file is part of kio_smtp, the KDE SMTP kioslave. + Copyright (c) 2003 Marc Mutz <mutz@kde.org> + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License, version 2, as + published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + In addition, as a special exception, the copyright holders give + permission to link the code of this program with any edition of + the Qt library by Trolltech AS, Norway (or with modified versions + of Qt that use the same license as Qt), and distribute linked + combinations including the two. You must obey the GNU General + Public License in all respects for all of the code used other than + Qt. If you modify this file, you may extend this exception to + your version of the file, but you are not obligated to do so. If + you do not wish to do so, delete this exception statement from + your version. +*/ + +#include <config.h> + +#include "command.h" + +#include "smtp.h" +#include "response.h" +#include "transactionstate.h" + +#include <kidna.h> +#include <klocale.h> +#include <kdebug.h> +#include <kmdcodec.h> +#include <kio/slavebase.h> // for test_commands, where SMTPProtocol is not derived from TCPSlaveBase + +#include <assert.h> + +namespace KioSMTP { + +#ifdef HAVE_LIBSASL2 +static sasl_callback_t callbacks[] = { + { SASL_CB_ECHOPROMPT, NULL, NULL }, + { SASL_CB_NOECHOPROMPT, NULL, NULL }, + { SASL_CB_GETREALM, NULL, NULL }, + { SASL_CB_USER, NULL, NULL }, + { SASL_CB_AUTHNAME, NULL, NULL }, + { SASL_CB_PASS, NULL, NULL }, + { SASL_CB_CANON_USER, NULL, NULL }, + { SASL_CB_LIST_END, NULL, NULL } +}; +#endif + + // + // Command (base class) + // + + Command::Command( SMTPProtocol * smtp, int flags ) + : mSMTP( smtp ), + mComplete( false ), mNeedResponse( false ), mFlags( flags ) + { + assert( smtp ); + } + + Command::~Command() {} + + bool Command::processResponse( const Response & r, TransactionState * ) { + mComplete = true; + mNeedResponse = false; + return r.isOk(); + } + + void Command::ungetCommandLine( const QCString &, TransactionState * ) { + mComplete = false; + } + + Command * Command::createSimpleCommand( int which, SMTPProtocol * smtp ) { + switch ( which ) { + case STARTTLS: return new StartTLSCommand( smtp ); + case DATA: return new DataCommand( smtp ); + case NOOP: return new NoopCommand( smtp ); + case RSET: return new RsetCommand( smtp ); + case QUIT: return new QuitCommand( smtp ); + default: return 0; + } + } + + // + // relay methods: + // + + void Command::parseFeatures( const Response & r ) { + mSMTP->parseFeatures( r ); + } + + int Command::startTLS() { + return mSMTP->startTLS(); + } + + bool Command::usingSSL() const { + return mSMTP->usingSSL(); + } + + bool Command::usingTLS() const { + return mSMTP->usingTLS(); + } + + bool Command::haveCapability( const char * cap ) const { + return mSMTP->haveCapability( cap ); + } + + // + // EHLO / HELO + // + + QCString EHLOCommand::nextCommandLine( TransactionState * ) { + mNeedResponse = true; + mComplete = mEHLONotSupported; + const char * cmd = mEHLONotSupported ? "HELO " : "EHLO " ; + return cmd + KIDNA::toAsciiCString( mHostname ) + "\r\n"; + } + + bool EHLOCommand::processResponse( const Response & r, TransactionState * ) { + mNeedResponse = false; + // "command not {recognized,implemented}" response: + if ( r.code() == 500 || r.code() == 502 ) { + if ( mEHLONotSupported ) { // HELO failed... + mSMTP->error( KIO::ERR_INTERNAL_SERVER, + i18n("The server rejected both EHLO and HELO commands " + "as unknown or unimplemented.\n" + "Please contact the server's system administrator.") ); + return false; + } + mEHLONotSupported = true; // EHLO failed, but that's ok. + return true; + } + mComplete = true; + if ( r.code() / 10 == 25 ) { // 25x: success + parseFeatures( r ); + return true; + } + mSMTP->error( KIO::ERR_UNKNOWN, + i18n("Unexpected server response to %1 command.\n%2") + .arg( mEHLONotSupported ? "HELO" : "EHLO" ) + .arg( r.errorMessage() ) ); + return false; + } + + // + // STARTTLS - rfc 3207 + // + + QCString StartTLSCommand::nextCommandLine( TransactionState * ) { + mComplete = true; + mNeedResponse = true; + return "STARTTLS\r\n"; + } + + bool StartTLSCommand::processResponse( const Response & r, TransactionState * ) { + mNeedResponse = false; + if ( r.code() != 220 ) { + mSMTP->error( r.errorCode(), + i18n("Your SMTP server does not support TLS. " + "Disable TLS, if you want to connect " + "without encryption.") ); + return false; + } + + int tlsrc = startTLS(); + + if ( tlsrc == 1 ) + return true; + + if ( tlsrc != -3 ) + //kdDebug(7112) << "TLS negotiation failed!" << endl; + mSMTP->messageBox(KIO::SlaveBase::Information, + i18n("Your SMTP server claims to " + "support TLS, but negotiation " + "was unsuccessful.\nYou can " + "disable TLS in KDE using the " + "crypto settings module."), + i18n("Connection Failed")); + return false; + } + + +#define SASLERROR mSMTP->error(KIO::ERR_COULD_NOT_AUTHENTICATE, \ + i18n("An error occured during authentication: %1").arg \ + ( QString::fromUtf8( sasl_errdetail( conn ) ))); + + // + // AUTH - rfc 2554 + // + AuthCommand::AuthCommand( SMTPProtocol * smtp, + const char *mechanisms, + const QString &aFQDN, + KIO::AuthInfo &ai ) + : Command( smtp, CloseConnectionOnError|OnlyLastInPipeline ), + mAi( &ai ), + mFirstTime( true ) + { +#ifdef HAVE_LIBSASL2 + int result; + mMechusing = 0; + conn = 0; + client_interact = 0; + mOut = 0; mOutlen = 0; + mOneStep = false; + + result = sasl_client_new( "smtp", aFQDN.latin1(), + 0, 0, callbacks, 0, &conn ); + if ( result != SASL_OK ) { + SASLERROR + return; + } + do { + result = sasl_client_start(conn, mechanisms, + &client_interact, &mOut, &mOutlen, &mMechusing); + + if (result == SASL_INTERACT) + if ( !saslInteract( client_interact ) ) { + return; + }; + } while ( result == SASL_INTERACT ); + if ( result != SASL_CONTINUE && result != SASL_OK ) { + SASLERROR + return; + } + if ( result == SASL_OK ) mOneStep = true; + kdDebug(7112) << "Mechanism: " << mMechusing << " one step: " << mOneStep << endl; +#else + mSMTP->error(KIO::ERR_COULD_NOT_AUTHENTICATE, + i18n("Authentication support is not compiled into kio_smtp.")); +#endif + } + + AuthCommand::~AuthCommand() + { +#ifdef HAVE_LIBSASL2 + if ( conn ) { + kdDebug(7112) << "dispose sasl connection" << endl; + sasl_dispose( &conn ); + conn = 0; + } +#endif + } + + bool AuthCommand::saslInteract( void *in ) + { +#ifdef HAVE_LIBSASL2 + kdDebug(7112) << "saslInteract: " << endl; + sasl_interact_t *interact = ( sasl_interact_t * ) in; + + //some mechanisms do not require username && pass, so don't need a popup + //window for getting this info + for ( ; interact->id != SASL_CB_LIST_END; interact++ ) { + if ( interact->id == SASL_CB_AUTHNAME || + interact->id == SASL_CB_PASS ) { + + if ( mAi->username.isEmpty() || mAi->password.isEmpty()) { + if (!mSMTP->openPassDlg(*mAi)) { + mSMTP->error(KIO::ERR_ABORTED, i18n("No authentication details supplied.")); + return false; + } + } + break; + } + } + + interact = ( sasl_interact_t * ) in; + while( interact->id != SASL_CB_LIST_END ) { + switch( interact->id ) { + case SASL_CB_USER: + case SASL_CB_AUTHNAME: + kdDebug(7112) << "SASL_CB_[USER|AUTHNAME]: " << mAi->username << endl; + interact->result = strdup( mAi->username.utf8() ); + interact->len = strlen( (const char *) interact->result ); + break; + case SASL_CB_PASS: + kdDebug(7112) << "SASL_CB_PASS: [HIDDEN]" << endl; + interact->result = strdup( mAi->password.utf8() ); + interact->len = strlen( (const char *) interact->result ); + break; + default: + interact->result = NULL; interact->len = 0; + break; + } + interact++; + } + return true; +#else + return false; +#endif + } + + bool AuthCommand::doNotExecute( const TransactionState * ) const { + return !mMechusing; + } + + void AuthCommand::ungetCommandLine( const QCString & s, TransactionState * ) { + mUngetSASLResponse = s; + mComplete = false; + } + + QCString AuthCommand::nextCommandLine( TransactionState * ) { + mNeedResponse = true; + QCString cmd; +#ifdef HAVE_LIBSASL2 + QByteArray tmp, challenge; + if ( !mUngetSASLResponse.isNull() ) { + // implement un-ungetCommandLine + cmd = mUngetSASLResponse; + mUngetSASLResponse = 0; + } else if ( mFirstTime ) { + QString firstCommand = "AUTH " + QString::fromLatin1( mMechusing ); + + tmp.setRawData( mOut, mOutlen ); + KCodecs::base64Encode( tmp, challenge ); + tmp.resetRawData( mOut, mOutlen ); + if ( !challenge.isEmpty() ) { + firstCommand += " "; + firstCommand += QString::fromLatin1( challenge.data(), challenge.size() ); + } + cmd = firstCommand.latin1(); + + if ( mOneStep ) mComplete = true; + } else { +// kdDebug(7112) << "SS: '" << mLastChallenge << "'" << endl; + tmp.setRawData( mLastChallenge.data(), mLastChallenge.length() ); + KCodecs::base64Decode( tmp, challenge ); + tmp.resetRawData( mLastChallenge.data(), mLastChallenge.length() ); + int result; + do { + result = sasl_client_step(conn, challenge.isEmpty() ? 0 : challenge.data(), + challenge.size(), + &client_interact, + &mOut, &mOutlen); + if (result == SASL_INTERACT) + if ( !saslInteract( client_interact ) ) { + return ""; + }; + } while ( result == SASL_INTERACT ); + if ( result != SASL_CONTINUE && result != SASL_OK ) { + kdDebug(7112) << "sasl_client_step failed with: " << result << endl; + SASLERROR + return ""; + } + tmp.setRawData( mOut, mOutlen ); + cmd = KCodecs::base64Encode( tmp ); + tmp.resetRawData( mOut, mOutlen ); + +// kdDebug(7112) << "CC: '" << cmd << "'" << endl; + mComplete = ( result == SASL_OK ); + } +#endif //HAVE_LIBSASL2 + cmd += "\r\n"; + return cmd; + } + + bool AuthCommand::processResponse( const Response & r, TransactionState * ) { + if ( !r.isOk() ) { + if ( mFirstTime ) + if ( haveCapability( "AUTH" ) ) + mSMTP->error( KIO::ERR_COULD_NOT_LOGIN, + i18n("Your SMTP server does not support %1.\nChoose a different authentication method.\n%2") + .arg( mMechusing ).arg( r.errorMessage() ) ); + else + mSMTP->error( KIO::ERR_COULD_NOT_LOGIN, + i18n("Your SMTP server does not support authentication.\n" + " %2").arg( r.errorMessage() ) ); + else + mSMTP->error( KIO::ERR_COULD_NOT_LOGIN, + i18n("Authentication failed.\n" + "Most likely the password is wrong.\n" + "%1").arg( r.errorMessage() ) ); + return false; + } + mFirstTime = false; + mLastChallenge = r.lines().front(); // ### better join all lines with \n? + mNeedResponse = false; + return true; + } + + // + // MAIL FROM: + // + + QCString MailFromCommand::nextCommandLine( TransactionState * ) { + mComplete = true; + mNeedResponse = true; + QCString cmdLine = "MAIL FROM:<" + mAddr + '>'; + if ( m8Bit && haveCapability("8BITMIME") ) + cmdLine += " BODY=8BITMIME"; + if ( mSize && haveCapability("SIZE") ) + cmdLine += " SIZE=" + QCString().setNum( mSize ); + return cmdLine + "\r\n"; + } + + bool MailFromCommand::processResponse( const Response & r, TransactionState * ts ) { + assert( ts ); + mNeedResponse = false; + + if ( r.code() == 250 ) + return true; + + ts->setMailFromFailed( mAddr, r ); + return false; + } + + // + // RCPT TO: + // + + QCString RcptToCommand::nextCommandLine( TransactionState * ) { + mComplete = true; + mNeedResponse = true; + return "RCPT TO:<" + mAddr + ">\r\n"; + } + + bool RcptToCommand::processResponse( const Response & r, TransactionState * ts ) { + assert( ts ); + mNeedResponse = false; + + if ( r.code() == 250 ) { + ts->setRecipientAccepted(); + return true; + } + + ts->addRejectedRecipient( mAddr, r.errorMessage() ); + return false; + } + + // + // DATA (only initial processing!) + // + + QCString DataCommand::nextCommandLine( TransactionState * ts ) { + assert( ts ); + mComplete = true; + mNeedResponse = true; + ts->setDataCommandIssued( true ); + return "DATA\r\n"; + } + + void DataCommand::ungetCommandLine( const QCString &, TransactionState * ts ) { + assert( ts ); + mComplete = false; + ts->setDataCommandIssued( false ); + } + + bool DataCommand::processResponse( const Response & r, TransactionState * ts ) { + assert( ts ); + mNeedResponse = false; + + if ( r.code() == 354 ) { + ts->setDataCommandSucceeded( true, r ); + return true; + } + + ts->setDataCommandSucceeded( false, r ); + return false; + } + + // + // DATA (data transfer) + // + void TransferCommand::ungetCommandLine( const QCString & cmd, TransactionState * ) { + if ( cmd.isEmpty() ) + return; // don't change state when we can't detect the unget in + // the next nextCommandLine !! + mWasComplete = mComplete; + mComplete = false; + mNeedResponse = false; + mUngetBuffer = cmd; + } + + bool TransferCommand::doNotExecute( const TransactionState * ts ) const { + assert( ts ); + return ts->failed(); + } + + QCString TransferCommand::nextCommandLine( TransactionState * ts ) { + assert( ts ); // let's rely on it ( at least for the moment ) + assert( !isComplete() ); + assert( !ts->failed() ); + + static const QCString dotCRLF = ".\r\n"; + static const QCString CRLFdotCRLF = "\r\n.\r\n"; + + if ( !mUngetBuffer.isEmpty() ) { + const QCString ret = mUngetBuffer; + mUngetBuffer = 0; + if ( mWasComplete ) { + mComplete = true; + mNeedResponse = true; + } + return ret; // don't prepare(), it's slave-generated or already prepare()d + } + + // normal processing: + + kdDebug(7112) << "requesting data" << endl; + mSMTP->dataReq(); + QByteArray ba; + int result = mSMTP->readData( ba ); + kdDebug(7112) << "got " << result << " bytes" << endl; + if ( result > 0 ) + return prepare( ba ); + else if ( result < 0 ) { + ts->setFailedFatally( KIO::ERR_INTERNAL, + i18n("Could not read data from application.") ); + mComplete = true; + mNeedResponse = true; + return 0; + } + mComplete = true; + mNeedResponse = true; + return mLastChar == '\n' ? dotCRLF : CRLFdotCRLF ; + } + + bool TransferCommand::processResponse( const Response & r, TransactionState * ts ) { + mNeedResponse = false; + assert( ts ); + ts->setComplete(); + if ( !r.isOk() ) { + ts->setFailed(); + mSMTP->error( r.errorCode(), + i18n("The message content was not accepted.\n" + "%1").arg( r.errorMessage() ) ); + return false; + } + return true; + } + + static QCString dotstuff_lf2crlf( const QByteArray & ba, char & last ) { + QCString result( ba.size() * 2 + 1 ); // worst case: repeated "[.]\n" + const char * s = ba.data(); + const char * const send = ba.data() + ba.size(); + char * d = result.data(); + + while ( s < send ) { + const char ch = *s++; + if ( ch == '\n' && last != '\r' ) + *d++ = '\r'; // lf2crlf + else if ( ch == '.' && last == '\n' ) + *d++ = '.'; // dotstuff + last = *d++ = ch; + } + + result.truncate( d - result.data() ); + return result; + } + + QCString TransferCommand::prepare( const QByteArray & ba ) { + if ( ba.isEmpty() ) + return 0; + if ( mSMTP->metaData("lf2crlf+dotstuff") == "slave" ) { + kdDebug(7112) << "performing dotstuffing and LF->CRLF transformation" << endl; + return dotstuff_lf2crlf( ba, mLastChar ); + } else { + mLastChar = ba[ ba.size() - 1 ]; + return QCString( ba.data(), ba.size() + 1 ); + } + } + + // + // NOOP + // + + QCString NoopCommand::nextCommandLine( TransactionState * ) { + mComplete = true; + mNeedResponse = true; + return "NOOP\r\n"; + } + + // + // RSET + // + + QCString RsetCommand::nextCommandLine( TransactionState * ) { + mComplete = true; + mNeedResponse = true; + return "RSET\r\n"; + } + + // + // QUIT + // + + QCString QuitCommand::nextCommandLine( TransactionState * ) { + mComplete = true; + mNeedResponse = true; + return "QUIT\r\n"; + } + +} // namespace KioSMTP + diff --git a/kioslave/smtp/command.h b/kioslave/smtp/command.h new file mode 100644 index 000000000..e67f02556 --- /dev/null +++ b/kioslave/smtp/command.h @@ -0,0 +1,283 @@ +/* -*- c++ -*- + command.h + + This file is part of kio_smtp, the KDE SMTP kioslave. + Copyright (c) 2003 Marc Mutz <mutz@kde.org> + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License, version 2, as + published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + In addition, as a special exception, the copyright holders give + permission to link the code of this program with any edition of + the Qt library by Trolltech AS, Norway (or with modified versions + of Qt that use the same license as Qt), and distribute linked + combinations including the two. You must obey the GNU General + Public License in all respects for all of the code used other than + Qt. If you modify this file, you may extend this exception to + your version of the file, but you are not obligated to do so. If + you do not wish to do so, delete this exception statement from + your version. +*/ + +#ifndef __KIOSMTP_COMMAND_H__ +#define __KIOSMTP_COMMAND_H__ + + +#include <qstring.h> +#include <qcstring.h> + +#ifdef HAVE_LIBSASL2 +extern "C" { +#include <sasl/sasl.h> +} +#endif + +#include <kio/authinfo.h> + +class SMTPProtocol; +class QStrIList; + +namespace KioSMTP { + + class Response; + class TransactionState; + + /** + * @short Represents an SMTP command + * + * Semantics: A command consists of a series of "command lines" + * (though that's stretching it a bit for @ref TransferJob and @ref + * AuthCommand) and responses. There's typically one response for + * one command line and the command is completed. + * + * However, some commands consist of a dialog (command line, + * response, command line, response,...) where each successive + * command line is dependant on the previously received response + * (and thus those commands are not pipelinable). That's why each + * command signals completion by having it's @ref #isComplete() + * method return true @em after the last command line to be sent, + * but @em before the last response to receive. @ref AuthCommand is + * the principal representative of this kind of command. Because + * @ref EHLOCommand automatically falls back to HELO in case EHLO + * isn't supported, it is also of this kind. If completion is + * signalled before the first command line is issued, it is not to + * be executed at all. + * + * Other commands need to send multiple "command lines" before + * receiving a single (final) response. @ref TransferCommand is the + * only representative of this kind of "command". That's why each + * command signals whether it now expects a response before being + * able to issue the next command line (if any) by having it's @ref + * #needsResponse() method return true. + * + * Commands whose @ref #nextCommandLine() does not support being + * called multiple times in a row without changing command state, + * must reimplement @ref #ungetCommandLine(). + **/ + class Command { + public: + enum Flags { + OnlyLastInPipeline = 1, + OnlyFirstInPipeline = 2, + CloseConnectionOnError = 4 + }; + + Command( SMTPProtocol * smtp, int flags=0 ); + virtual ~Command(); + + enum Type { + STARTTLS, DATA, NOOP, RSET, QUIT + }; + + static Command * createSimpleCommand( int which, SMTPProtocol * smtp ); + + virtual QCString nextCommandLine( TransactionState * ts=0 ) = 0; + /* Reimplement this if your @ref #nextCommandLine() implementation + changes state other than @ref mComplete. The default + implementation just resets @ref mComplete to false. */ + virtual void ungetCommandLine( const QCString & cmdLine, TransactionState * ts=0 ); + /* Reimplement this if your command need more sophisicated + response processing than just checking for @ref + Response::isOk(). The default implementation sets @ref + mComplete to true, @ref mNeedResponse to false and returns + whether the response isOk(). */ + virtual bool processResponse( const Response & response, TransactionState * ts=0 ); + + virtual bool doNotExecute( const TransactionState * ) const { return false; } + + bool isComplete() const { return mComplete; } + /** @return whether the command expects a response now. Some + commands (most notably AUTH) may consist of a series of + commands and associated responses until they are + complete. Others (most notably @ref TransferCommand usually + send multiple "command lines" before expecting a response. */ + bool needsResponse() const { return mNeedResponse; } + /** @return whether an error in executing this command is so fatal + that closing the connection is the only option */ + bool closeConnectionOnError() const { + return mFlags & CloseConnectionOnError; + } + bool mustBeLastInPipeline() const { + return mFlags & OnlyLastInPipeline; + } + bool mustBeFirstInPipeline() const { + return mFlags & OnlyFirstInPipeline; + } + + protected: + SMTPProtocol * mSMTP; + bool mComplete; + bool mNeedResponse; + const int mFlags; + + protected: + // only relay methods to enable access to slave-protected methods + // for subclasses of Command: + void parseFeatures( const Response & r ); + int startTLS(); + bool usingSSL() const; + bool usingTLS() const; + bool haveCapability( const char * cap ) const; + }; + + class EHLOCommand : public Command { + public: + EHLOCommand( SMTPProtocol * smtp, const QString & hostname ) + : Command( smtp, CloseConnectionOnError|OnlyLastInPipeline ), + mEHLONotSupported( false ), + mHostname( hostname.stripWhiteSpace() ) {} + + QCString nextCommandLine( TransactionState * ); + bool processResponse( const Response & response, TransactionState * ); + private: + bool mEHLONotSupported; + QString mHostname; + }; + + class StartTLSCommand : public Command { + public: + StartTLSCommand( SMTPProtocol * smtp ) + : Command( smtp, CloseConnectionOnError|OnlyLastInPipeline ) {} + + QCString nextCommandLine( TransactionState * ); + bool processResponse( const Response & response, TransactionState * ); + }; + + class AuthCommand : public Command { + public: + AuthCommand( SMTPProtocol * smtp, const char *mechanisms, + const QString &aFQDN, KIO::AuthInfo &ai ); + ~AuthCommand(); + bool doNotExecute( const TransactionState * ts ) const; + QCString nextCommandLine( TransactionState * ); + void ungetCommandLine( const QCString & cmdLine, TransactionState * ); + bool processResponse( const Response & response, TransactionState * ); + private: + bool saslInteract( void *in ); + +#ifdef HAVE_LIBSASL2 + sasl_conn_t *conn; + sasl_interact_t *client_interact; +#endif + const char *mOut, *mMechusing; + uint mOutlen; + bool mOneStep; + + KIO::AuthInfo *mAi; + QCString mLastChallenge; + QCString mUngetSASLResponse; + bool mFirstTime; + }; + + class MailFromCommand : public Command { + public: + MailFromCommand( SMTPProtocol * smtp, const QCString & addr, + bool eightBit=false, unsigned int size=0 ) + : Command( smtp ), mAddr( addr ), m8Bit( eightBit ), mSize( size ) {} + + QCString nextCommandLine( TransactionState * ); + bool processResponse( const Response & response, TransactionState * ); + private: + QCString mAddr; + bool m8Bit; + unsigned int mSize; + }; + + class RcptToCommand : public Command { + public: + RcptToCommand( SMTPProtocol * smtp, const QCString & addr ) + : Command( smtp ), mAddr( addr ) {} + + QCString nextCommandLine( TransactionState * ); + bool processResponse( const Response & response, TransactionState * ); + private: + QCString mAddr; + }; + + /** Handles only the initial intermediate response and compltetes at + the point where the mail contents need to be sent */ + class DataCommand : public Command { + public: + DataCommand( SMTPProtocol * smtp ) + : Command( smtp, OnlyLastInPipeline ) {} + + QCString nextCommandLine( TransactionState * ); + void ungetCommandLine( const QCString & cmd, TransactionState * ts ); + bool processResponse( const Response & response, TransactionState * ); + }; + + /** Handles the data transfer following a successful DATA command */ + class TransferCommand : public Command { + public: + TransferCommand( SMTPProtocol * smtp, const QCString & initialBuffer ) + : Command( smtp, OnlyFirstInPipeline ), + mUngetBuffer( initialBuffer ), mLastChar( '\n' ), mWasComplete( false ) {} + + bool doNotExecute( const TransactionState * ts ) const; + QCString nextCommandLine( TransactionState * ); + void ungetCommandLine( const QCString & cmd, TransactionState * ts ); + bool processResponse( const Response & response, TransactionState * ); + private: + QCString prepare( const QByteArray & ba ); + QCString mUngetBuffer; + char mLastChar; + bool mWasComplete; // ... before ungetting + }; + + class NoopCommand : public Command { + public: + NoopCommand( SMTPProtocol * smtp ) + : Command( smtp, OnlyLastInPipeline ) {} + + QCString nextCommandLine( TransactionState * ); + }; + + class RsetCommand : public Command { + public: + RsetCommand( SMTPProtocol * smtp ) + : Command( smtp, CloseConnectionOnError ) {} + + QCString nextCommandLine( TransactionState * ); + }; + + class QuitCommand : public Command { + public: + QuitCommand( SMTPProtocol * smtp ) + : Command( smtp, CloseConnectionOnError|OnlyLastInPipeline ) {} + + QCString nextCommandLine( TransactionState * ); + }; + +} // namespace KioSMTP + +#endif // __KIOSMTP_COMMAND_H__ diff --git a/kioslave/smtp/compliance.txt b/kioslave/smtp/compliance.txt new file mode 100644 index 000000000..b6b9874c8 --- /dev/null +++ b/kioslave/smtp/compliance.txt @@ -0,0 +1,33 @@ +The SMTP kioslave currently conforms to the following SMTP-related RFCs: + +Base Spec: +2821 Simple Mail Transfer Protocol. J. Klensin, Ed.. April 2001. + (Format: TXT=192504 bytes) (Obsoletes RFC0821, RFC0974, RFC1869) + (Status: PROPOSED STANDARD) + +Encryption/Auth: +3207 SMTP Service Extension for Secure SMTP over Transport Layer + Security. P. Hoffman. February 2002. (Format: TXT=18679 bytes) + (Obsoletes RFC2487) (Status: PROPOSED STANDARD) + +2554 SMTP Service Extension for Authentication. J. Myers. March 1999. + (Format: TXT=20534 bytes) (Status: PROPOSED STANDARD) +(with all SASL mechanisms supported by KDESasl) + +General: +1652 SMTP Service Extension for 8bit-MIMEtransport. J. Klensin, N. + Freed, M. Rose, E. Stefferud, D. Crocker. July 1994. (Format: + TXT=11842 bytes) (Obsoletes RFC1426) (Status: DRAFT STANDARD) + +1870 SMTP Service Extension for Message Size Declaration. J. Klensin, + N. Freed, K. Moore. November 1995. (Format: TXT=18226 bytes) + (Obsoletes RFC1653) (Also STD0010) (Status: STANDARD) + +2920 SMTP Service Extension for Command Pipelining. N. Freed. + September 2000. (Format: TXT=17065 bytes) (Obsoletes RFC2197) (Also + STD0060) (Status: STANDARD) + +Known shortcomings: +- Doesn't enforce the CRLF lineending convention on user-supplied data. +- Due to the lack of a Mulit_Put_ in the KIO infrastructure, pipelining + across messages isn't supported. diff --git a/kioslave/smtp/interactivesmtpserver.cc b/kioslave/smtp/interactivesmtpserver.cc new file mode 100644 index 000000000..4deddd3ca --- /dev/null +++ b/kioslave/smtp/interactivesmtpserver.cc @@ -0,0 +1,127 @@ +/* -*- c++ -*- + interactivesmtpserver.cc + + Code based on the serverSocket example by Jesper Pedersen. + + This file is part of the testsuite of kio_smtp, the KDE SMTP kioslave. + Copyright (c) 2004 Marc Mutz <mutz@kde.org> + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License, version 2, as + published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + In addition, as a special exception, the copyright holders give + permission to link the code of this program with any edition of + the Qt library by Trolltech AS, Norway (or with modified versions + of Qt that use the same license as Qt), and distribute linked + combinations including the two. You must obey the GNU General + Public License in all respects for all of the code used other than + Qt. If you modify this file, you may extend this exception to + your version of the file, but you are not obligated to do so. If + you do not wish to do so, delete this exception statement from + your version. +*/ + +#include <config.h> + +#include <qserversocket.h> +#include <qsocket.h> +#include <qwidget.h> +#include <qapplication.h> +#include <qhostaddress.h> +#include <qtextedit.h> +#include <qlineedit.h> +#include <qlabel.h> +#include <qstring.h> +#include <qlayout.h> +#include <qpushbutton.h> + +#include <cassert> + +#include "interactivesmtpserver.h" + +static const QHostAddress localhost( 0x7f000001 ); // 127.0.0.1 + +InteractiveSMTPServerWindow::~InteractiveSMTPServerWindow() { + if ( mSocket ) { + mSocket->close(); + if ( mSocket->state() == QSocket::Closing ) + connect( mSocket, SIGNAL(delayedCloseFinished()), + mSocket, SLOT(deleteLater()) ); + else + mSocket->deleteLater(); + mSocket = 0; + } +} + +void InteractiveSMTPServerWindow::slotSendResponse() +{ + const QString line = mLineEdit->text(); + mLineEdit->clear(); + QTextStream s( mSocket ); + s << line + "\r\n"; + slotDisplayServer( line ); +} + +InteractiveSMTPServer::InteractiveSMTPServer( QObject* parent ) + : QServerSocket( localhost, 2525, 1, parent ) +{ +} + +int main( int argc, char * argv[] ) { + QApplication app( argc, argv ); + + InteractiveSMTPServer server; + + qDebug( "Server should now listen on localhost:2525" ); + qDebug( "Hit CTRL-C to quit." ); + return app.exec(); +}; + + +InteractiveSMTPServerWindow::InteractiveSMTPServerWindow( QSocket * socket, QWidget * parent, const char * name, WFlags f ) + : QWidget( parent, name, f ), mSocket( socket ) +{ + QPushButton * but; + assert( socket ); + + QVBoxLayout * vlay = new QVBoxLayout( this, 6 ); + + mTextEdit = new QTextEdit( this ); + mTextEdit->setTextFormat( QTextEdit::LogText ); + vlay->addWidget( mTextEdit, 1 ); + + QHBoxLayout * hlay = new QHBoxLayout( vlay ); + + mLineEdit = new QLineEdit( this ); + but = new QPushButton( "&Send", this ); + hlay->addWidget( new QLabel( mLineEdit, "&Response:", this ) ); + hlay->addWidget( mLineEdit, 1 ); + hlay->addWidget( but ); + + connect( mLineEdit, SIGNAL(returnPressed()), SLOT(slotSendResponse()) ); + connect( but, SIGNAL(clicked()), SLOT(slotSendResponse()) ); + + but = new QPushButton( "&Close Connection", this ); + vlay->addWidget( but ); + + connect( but, SIGNAL(clicked()), SLOT(slotConnectionClosed()) ); + + connect( socket, SIGNAL(connectionClosed()), SLOT(slotConnectionClosed()) ); + connect( socket, SIGNAL(error(int)), SLOT(slotError(int)) ); + connect( socket, SIGNAL(readyRead()), SLOT(slotReadyRead()) ); + + mLineEdit->setText( "220 hi there" ); + mLineEdit->setFocus(); +} + +#include "interactivesmtpserver.moc" diff --git a/kioslave/smtp/interactivesmtpserver.h b/kioslave/smtp/interactivesmtpserver.h new file mode 100644 index 000000000..3ae210a74 --- /dev/null +++ b/kioslave/smtp/interactivesmtpserver.h @@ -0,0 +1,121 @@ +#ifndef INTERACTIVESMTPSERVER_H +#define INTERACTIVESMTPSERVER_H + +/* -*- c++ -*- + interactivesmtpserver.h + + Code based on the serverSocket example by Jesper Pedersen. + + This file is part of the testsuite of kio_smtp, the KDE SMTP kioslave. + Copyright (c) 2004 Marc Mutz <mutz@kde.org> + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License, version 2, as + published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + In addition, as a special exception, the copyright holders give + permission to link the code of this program with any edition of + the Qt library by Trolltech AS, Norway (or with modified versions + of Qt that use the same license as Qt), and distribute linked + combinations including the two. You must obey the GNU General + Public License in all respects for all of the code used other than + Qt. If you modify this file, you may extend this exception to + your version of the file, but you are not obligated to do so. If + you do not wish to do so, delete this exception statement from + your version. +*/ + +#include <qwidget.h> + + +static QString err2str( int err ) { + switch ( err ) { + case QSocket::ErrConnectionRefused: return "Connection refused"; + case QSocket::ErrHostNotFound: return "Host not found"; + case QSocket::ErrSocketRead: return "Failed to read from socket"; + default: return "Unknown error"; + } +} + + +static QString escape( QString s ) { + return s + .replace( '&', "&" ) + .replace( '>', ">" ) + .replace( '<', "<" ) + .replace( '"', """ ) + ; +} + + +static QString trim( const QString & s ) { + if ( s.endsWith( "\r\n" ) ) + return s.left( s.length() - 2 ); + if ( s.endsWith( "\r" ) || s.endsWith( "\n" ) ) + return s.left( s.length() - 1 ); + return s; +} + + +class InteractiveSMTPServerWindow : public QWidget { + Q_OBJECT +public: + InteractiveSMTPServerWindow( QSocket * socket, QWidget * parent=0, const char * name=0, WFlags f=0 ); + ~InteractiveSMTPServerWindow(); + +public slots: + void slotSendResponse(); + void slotDisplayClient( const QString & s ) { + mTextEdit->append( "C:" + escape(s) ); + } + void slotDisplayServer( const QString & s ) { + mTextEdit->append( "S:" + escape(s) ); + } + void slotDisplayMeta( const QString & s ) { + mTextEdit->append( "<font color=\"red\">" + escape(s) + "</font>" ); + } + void slotReadyRead() { + while ( mSocket->canReadLine() ) + slotDisplayClient( trim( mSocket->readLine() ) ); + } + void slotError( int err ) { + slotDisplayMeta( QString( "E: %1 (%2)" ).arg( err2str( err ) ).arg( err ) ); + } + void slotConnectionClosed() { + slotDisplayMeta( "Connection closed by peer" ); + } + void slotCloseConnection() { + mSocket->close(); + } +private: + QSocket * mSocket; + QTextEdit * mTextEdit; + QLineEdit * mLineEdit; +}; + +class InteractiveSMTPServer : public QServerSocket { + Q_OBJECT +public: + InteractiveSMTPServer( QObject * parent=0 ); + ~InteractiveSMTPServer() {} + + /*! \reimp */ + void newConnection( int fd ) { + QSocket * socket = new QSocket(); + socket->setSocket( fd ); + InteractiveSMTPServerWindow * w = new InteractiveSMTPServerWindow( socket ); + w->show(); + } +}; + + +#endif diff --git a/kioslave/smtp/request.cc b/kioslave/smtp/request.cc new file mode 100644 index 000000000..566d2c758 --- /dev/null +++ b/kioslave/smtp/request.cc @@ -0,0 +1,189 @@ +/* -*- c++ -*- + request.cc + + This file is part of kio_smtp, the KDE SMTP kioslave. + Copyright (c) 2003 Marc Mutz <mutz@kde.org> + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License, version 2, as + published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + In addition, as a special exception, the copyright holders give + permission to link the code of this program with any edition of + the Qt library by Trolltech AS, Norway (or with modified versions + of Qt that use the same license as Qt), and distribute linked + combinations including the two. You must obey the GNU General + Public License in all respects for all of the code used other than + Qt. If you modify this file, you may extend this exception to + your version of the file, but you are not obligated to do so. If + you do not wish to do so, delete this exception statement from + your version. +*/ + +#include <config.h> + +#include "request.h" + +#include <kurl.h> +#include <kidna.h> +#include <kmdcodec.h> +#include <kdebug.h> + +#include <assert.h> + +namespace KioSMTP { + + Request Request::fromURL( const KURL & url ) { + Request request; + + const QStringList query = QStringList::split( '&', url.query().mid(1) ); +#ifndef NDEBUG + kdDebug(7112) << "Parsing request from query:\n" + query.join("\n" ) << endl; +#endif + for ( QStringList::const_iterator it = query.begin() ; it != query.end() ; ++it ) { + int equalsPos = (*it).find( '=' ); + if ( equalsPos <= 0 ) + continue; + + const QString key = (*it).left( equalsPos ).lower(); + const QString value = KURL::decode_string( (*it).mid( equalsPos + 1 ) ); + + if ( key == "to" ) + request.addTo( value ); + else if ( key == "cc" ) + request.addCc( value ); + else if ( key == "bcc" ) + request.addBcc( value ); + else if ( key == "headers" ) { + request.setEmitHeaders( value == "0" ); + request.setEmitHeaders( false ); // ### ??? + } + else if ( key == "subject" ) + request.setSubject( value ); + else if ( key == "from" ) + request.setFromAddress( value ); + else if ( key == "profile" ) + request.setProfileName( value ); + else if ( key == "hostname" ) + request.setHeloHostname( value ); + else if ( key == "body" ) + request.set8BitBody( value.upper() == "8BIT" ); + else if ( key == "size" ) + request.setSize( value.toUInt() ); + else + kdWarning(7112) << "while parsing query: unknown query item \"" + << key << "\" with value \"" << value << "\"" << endl; + } + + return request; + } + + QCString Request::heloHostnameCString() const { + return KIDNA::toAsciiCString( heloHostname() ); + } + + static bool isUsAscii( const QString & s ) { + for ( uint i = 0 ; i < s.length() ; ++i ) + if ( s[i].unicode() > 127 ) return false; + return true; + } + + + + static inline bool isSpecial( char ch ) { + static const QCString specials = "()<>[]:;@\\,.\""; + return specials.find( ch ) >= 0; + } + + + + static inline bool needsQuoting( char ch ) { + return ch == '\\' || ch == '"' || ch == '\n' ; + } + + + + static inline QCString rfc2047Encode( const QString & s ) { + QCString r = KCodecs::base64Encode( s.stripWhiteSpace().utf8(), false ); + return "=?utf-8?b?" + r + "?=" ; // use base64 since that always gives a valid encoded-word + } + + + + static QCString quote( const QString & s ) { + assert( isUsAscii( s ) ); + + QCString r( s.length() * 2 ); + bool needsQuotes = false; + + unsigned int j = 0; + for ( unsigned int i = 0 ; i < s.length() ; ++i ) { + char ch = s[i].latin1(); + if ( isSpecial( ch ) ) { + if ( needsQuoting( ch ) ) + r[j++] = '\\'; + needsQuotes = true; + } + r[j++] = ch; + } + r.truncate( j ); + + if ( needsQuotes ) + return '"' + r + '"'; + else + return r; + } + + + + static QCString formatFromAddress( const QString & fromRealName, const QString & fromAddress ) { + if ( fromRealName.isEmpty() ) + return fromAddress.latin1(); // no real name: return "joe@user.org" + + // return "Joe User <joe@user.org>", "\"User, Joe\" <joe@user.org>" + // or "=?utf-8?q?Joe_User?= <joe@user.org>", depending on real name's nature. + QCString r = isUsAscii( fromRealName ) ? quote( fromRealName ) : rfc2047Encode( fromRealName ); + return r + " <" + fromAddress.latin1() + '>'; + } + + + + static QCString formatSubject( QString s ) { + if ( isUsAscii( s ) ) + return s.remove( '\n' ).latin1(); // don't break header folding, + // so remove any line break + // that happen to be around + else + return rfc2047Encode( s ); + } + + + + QCString Request::headerFields( const QString & fromRealName ) const { + if ( !emitHeaders() ) + return 0; + + assert( hasFromAddress() ); // should have been checked for by + // caller (MAIL FROM comes before DATA) + + QCString result = "From: " + formatFromAddress( fromRealName, fromAddress() ) + "\r\n"; + + if ( !subject().isEmpty() ) + result += "Subject: " + formatSubject( subject() ) + "\r\n"; + if ( !to().empty() ) + result += QCString( "To: " ) + to().join( ",\r\n\t" /* line folding */ ).latin1() + "\r\n"; + if ( !cc().empty() ) + result += QCString( "Cc: " ) + cc().join( ",\r\n\t" /* line folding */ ).latin1() + "\r\n"; + return result; + } + +} // namespace KioSMTP diff --git a/kioslave/smtp/request.h b/kioslave/smtp/request.h new file mode 100644 index 000000000..15bbd76b9 --- /dev/null +++ b/kioslave/smtp/request.h @@ -0,0 +1,100 @@ +/* -*- c++ -*- + request.h + + This file is part of kio_smtp, the KDE SMTP kioslave. + Copyright (c) 2003 Marc Mutz <mutz@kde.org> + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License, version 2, as + published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + In addition, as a special exception, the copyright holders give + permission to link the code of this program with any edition of + the Qt library by Trolltech AS, Norway (or with modified versions + of Qt that use the same license as Qt), and distribute linked + combinations including the two. You must obey the GNU General + Public License in all respects for all of the code used other than + Qt. If you modify this file, you may extend this exception to + your version of the file, but you are not obligated to do so. If + you do not wish to do so, delete this exception statement from + your version. +*/ + +#ifndef __KIOSMTP_REQUEST_H__ +#define __KIOSMTP_REQUEST_H__ + +#include <qstring.h> +#include <qstringlist.h> + +class KURL; + +namespace KioSMTP { + + class Request { + public: + Request() + : mSubject( "missing subject" ), mEmitHeaders( true ), + m8Bit( false ), mSize( 0 ) {} + + static Request fromURL( const KURL & url ); + + QString profileName() const { return mProfileName; } + void setProfileName( const QString & profileName ) { mProfileName = profileName; } + bool hasProfile() const { return !profileName().isNull(); } + + QString subject() const { return mSubject; } + void setSubject( const QString & subject ) { mSubject = subject; } + + QString fromAddress() const { return mFromAddress; } + void setFromAddress( const QString & fromAddress ) { mFromAddress = fromAddress; } + bool hasFromAddress() const { return !mFromAddress.isEmpty(); } + + QStringList recipients() const { return to() + cc() + bcc() ; } + bool hasRecipients() const { return !to().empty() || !cc().empty() || !bcc().empty() ; } + + QStringList to() const { return mTo; } + QStringList cc() const { return mCc; } + QStringList bcc() const { return mBcc; } + void addTo( const QString & to ) { mTo.push_back( to ); } + void addCc( const QString & cc ) { mCc.push_back( cc ); } + void addBcc( const QString & bcc ) { mBcc.push_back( bcc ); } + + QString heloHostname() const { return mHeloHostname; } + QCString heloHostnameCString() const; + void setHeloHostname( const QString & hostname ) { mHeloHostname = hostname; } + + bool emitHeaders() const { return mEmitHeaders; } + void setEmitHeaders( bool emitHeaders ) { mEmitHeaders = emitHeaders; } + + bool is8BitBody() const { return m8Bit; } + void set8BitBody( bool a8Bit ) { m8Bit = a8Bit; } + + unsigned int size() const { return mSize; } + void setSize( unsigned int size ) { mSize = size; } + + /** If @ref #emitHeaders() is true, returns the rfc2822 + serialization of the header fields "To", "Cc", "Subject" and + "From", as determined by the respective settings. If @ref + #emitHeaders() is false, returns a null string. */ + QCString headerFields( const QString & fromRealName=QString::null ) const; + + private: + QStringList mTo, mCc, mBcc; + QString mProfileName, mSubject, mFromAddress, mHeloHostname; + bool mEmitHeaders; + bool m8Bit; + unsigned int mSize; + }; + +} // namespace KioSMTP + +#endif // __KIOSMTP_REQUEST_H__ diff --git a/kioslave/smtp/response.cc b/kioslave/smtp/response.cc new file mode 100644 index 000000000..b1745865a --- /dev/null +++ b/kioslave/smtp/response.cc @@ -0,0 +1,160 @@ +/* -*- c++ -*- + response.cc + + This file is part of kio_smtp, the KDE SMTP kioslave. + Copyright (c) 2003 Marc Mutz <mutz@kde.org> + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License, version 2, as + published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + In addition, as a special exception, the copyright holders give + permission to link the code of this program with any edition of + the Qt library by Trolltech AS, Norway (or with modified versions + of Qt that use the same license as Qt), and distribute linked + combinations including the two. You must obey the GNU General + Public License in all respects for all of the code used other than + Qt. If you modify this file, you may extend this exception to + your version of the file, but you are not obligated to do so. If + you do not wish to do so, delete this exception statement from + your version. +*/ + +#include <config.h> + +#include "response.h" + +#include <klocale.h> +#include <kio/global.h> + +#include <qstring.h> + +namespace KioSMTP { + + void Response::parseLine( const char * line, int len ) { + + if ( !isWellFormed() ) return; // don't bother + + if ( isComplete() ) + // if the response is already complete, there can't be another line + mValid = false; + + if ( len > 1 && line[len-1] == '\n' && line[len-2] == '\r' ) + len -= 2; + + if ( len < 3 ) { + // can't be valid - too short + mValid = false; + mWellFormed = false; + return; + } + + bool ok = false; + unsigned int code = QCString( line, 3+1 ).toUInt( &ok ); + if ( !ok || code < 100 || code > 559 ) { + // not a number or number out of range + mValid = false; + if ( !ok || code < 100 ) + mWellFormed = false; + return; + } + if ( mCode && code != mCode ) { + // different codes in one response are not allowed. + mValid = false; + return; + } + mCode = code; + + if ( len == 3 || line[3] == ' ' ) + mSawLastLine = true; + else if ( line[3] != '-' ) { + // code must be followed by either SP or hyphen (len == 3 is + // also accepted since broken servers exist); all else is + // invalid + mValid = false; + mWellFormed = false; + return; + } + + mLines.push_back( len > 4 ? QCString( line+4, len-4+1 ).stripWhiteSpace() : QCString() ); + } + + + // hackishly fixing QCStringList flaws... + static QCString join( char sep, const QCStringList & list ) { + if ( list.empty() ) + return QCString(); + QCString result = list.front(); + for ( QCStringList::const_iterator it = ++list.begin() ; it != list.end() ; ++it ) + result += sep + *it; + return result; + } + + QString Response::errorMessage() const { + QString msg; + if ( lines().count() > 1 ) + msg = i18n("The server responded:\n%1") + .arg( join( '\n', lines() ) ); + else + msg = i18n("The server responded: \"%1\"") + .arg( lines().front() ); + if ( first() == 4 ) + msg += '\n' + i18n("This is a temporary failure. " + "You may try again later."); + return msg; + } + + int Response::errorCode() const { + switch ( code() ) { + case 421: // Service not available, closing transmission channel + case 454: // TLS not available due to temporary reason + // Temporary authentication failure + case 554: // Transaction failed / No SMTP service here / No valid recipients + return KIO::ERR_SERVICE_NOT_AVAILABLE; + + case 451: // Requested action aborted: local error in processing + return KIO::ERR_INTERNAL_SERVER; + + case 452: // Requested action not taken: insufficient system storage + case 552: // Requested mail action aborted: exceeded storage allocation + return KIO::ERR_DISK_FULL; + + case 500: // Syntax error, command unrecognized + case 501: // Syntax error in parameters or arguments + case 502: // Command not implemented + case 503: // Bad sequence of commands + case 504: // Command parameter not implemented + return KIO::ERR_INTERNAL; + + case 450: // Requested mail action not taken: mailbox unavailable + case 550: // Requested action not taken: mailbox unavailable + case 551: // User not local; please try <forward-path> + case 553: // Requested action not taken: mailbox name not allowed + return KIO::ERR_DOES_NOT_EXIST; + + case 530: // {STARTTLS,Authentication} required + case 538: // Encryption required for requested authentication mechanism + case 534: // Authentication mechanism is too weak + return KIO::ERR_UPGRADE_REQUIRED; + + case 432: // A password transition is needed + return KIO::ERR_COULD_NOT_AUTHENTICATE; + + default: + if ( isPositive() ) + return 0; + else + return KIO::ERR_UNKNOWN; + } + } + +} // namespace KioSMTP diff --git a/kioslave/smtp/response.h b/kioslave/smtp/response.h new file mode 100644 index 000000000..22835690a --- /dev/null +++ b/kioslave/smtp/response.h @@ -0,0 +1,125 @@ +/* -*- c++ -*- + response.h + + This file is part of kio_smtp, the KDE SMTP kioslave. + Copyright (c) 2003 Marc Mutz <mutz@kde.org> + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License, version 2, as + published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + In addition, as a special exception, the copyright holders give + permission to link the code of this program with any edition of + the Qt library by Trolltech AS, Norway (or with modified versions + of Qt that use the same license as Qt), and distribute linked + combinations including the two. You must obey the GNU General + Public License in all respects for all of the code used other than + Qt. If you modify this file, you may extend this exception to + your version of the file, but you are not obligated to do so. If + you do not wish to do so, delete this exception statement from + your version. +*/ + +#ifndef __KIOSMTP_RESPONSE_H__ +#define __KIOSMTP_RESPONSE_H__ + +#include <qcstring.h> +#include <qvaluelist.h> +typedef QValueList<QCString> QCStringList; + +class QString; + +namespace KioSMTP { + + class Response { + public: + Response() + : mCode(0), + mValid(true), + mSawLastLine(false), + mWellFormed(true) {} + + void parseLine( const char * line ) { + parseLine( line, qstrlen( line ) ); + } + void parseLine( const char * line, int len ); + + /** Return an internationalized error message according to the + response's code. */ + QString errorMessage() const; + /** Translate the SMTP error code into a KIO one */ + int errorCode() const; + + enum Reply { + UnknownReply = -1, + PositivePreliminary = 1, + PositiveCompletion = 2, + PositiveIntermediate = 3, + TransientNegative = 4, + PermanentNegative = 5 + }; + + enum Category { + UnknownCategory = -1, + SyntaxError = 0, + Information = 1, + Connections = 2, + MailSystem = 5 + }; + + unsigned int code() const { return mCode; } + unsigned int first() const { return code() / 100 ; } + unsigned int second() const { return ( code() % 100 ) / 10 ; } + unsigned int third() const { return code() % 10 ; } + + bool isPositive() const { return first() <= 3 && first() >= 1 ; } + bool isNegative() const { return first() == 4 || first() == 5 ; } + bool isUnknown() const { return !isPositive() && !isNegative() ; } + + QCStringList lines() const { return mLines; } + + bool isValid() const { return mValid; } + bool isComplete() const { return mSawLastLine; } + + /** Shortcut method. + @return true iff the response is valid, complete and positive */ + bool isOk() const { return isValid() && isComplete() && isPositive() ; } + /** Indicates whether the response was well-formed, meaning it + obeyed the syntax of smtp responses. That the response + nevertheless is not valid may be caused by e.g. different + response codes in a multilie response. A non-well-formed + response is never valid. */ + bool isWellFormed() const { return mWellFormed; } + + void clear() { *this = Response(); } + +#ifdef KIOSMTP_COMPARATORS + bool operator==( const Response & other ) const { + return mCode == other.mCode && + mValid == other.mValid && + mSawLastLine == other.mSawLastLine && + mWellFormed == other.mWellFormed && + mLines == other.mLines; + } +#endif + + private: + unsigned int mCode; + QCStringList mLines; + bool mValid; + bool mSawLastLine; + bool mWellFormed; + }; + +} // namespace KioSMTP + +#endif // __KIOSMTP_RESPONSE_H__ diff --git a/kioslave/smtp/smtp.cc b/kioslave/smtp/smtp.cc new file mode 100644 index 000000000..dc621f533 --- /dev/null +++ b/kioslave/smtp/smtp.cc @@ -0,0 +1,647 @@ +/* + * Copyright (c) 2000, 2001 Alex Zepeda <zipzippy@sonic.net> + * Copyright (c) 2001 Michael Häckel <Michael@Haeckel.Net> + * Copyright (c) 2002 Aaron J. Seigo <aseigo@olympusproject.org> + * Copyright (c) 2003 Marc Mutz <mutz@kde.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + */ + +#include <config.h> + +#ifdef HAVE_LIBSASL2 +extern "C" { +#include <sasl/sasl.h> +} +#endif + +#include "smtp.h" +#include "request.h" +#include "response.h" +#include "transactionstate.h" +#include "command.h" +using KioSMTP::Capabilities; +using KioSMTP::Command; +using KioSMTP::EHLOCommand; +using KioSMTP::AuthCommand; +using KioSMTP::MailFromCommand; +using KioSMTP::RcptToCommand; +using KioSMTP::DataCommand; +using KioSMTP::TransferCommand; +using KioSMTP::Request; +using KioSMTP::Response; +using KioSMTP::TransactionState; + +#include <kemailsettings.h> +#include <ksock.h> +#include <kdebug.h> +#include <kinstance.h> +#include <kio/connection.h> +#include <kio/slaveinterface.h> +#include <klocale.h> + +#include <qstring.h> +#include <qstringlist.h> +#include <qcstring.h> + +#include <memory> +using std::auto_ptr; + +#include <ctype.h> +#include <stdlib.h> +#include <string.h> +#include <stdio.h> +#include <assert.h> + +#ifdef HAVE_SYS_TYPES_H +# include <sys/types.h> +#endif +#ifdef HAVE_SYS_SOCKET_H +# include <sys/socket.h> +#endif +#include <netdb.h> + +#ifndef NI_NAMEREQD +// FIXME for KDE 3.3: fake defintion +// API design flaw in KExtendedSocket::resolve +# define NI_NAMEREQD 0 +#endif + + +extern "C" { + KDE_EXPORT int kdemain(int argc, char **argv); +} + +int kdemain(int argc, char **argv) +{ + KInstance instance("kio_smtp"); + + if (argc != 4) { + fprintf(stderr, + "Usage: kio_smtp protocol domain-socket1 domain-socket2\n"); + exit(-1); + } + +#ifdef HAVE_LIBSASL2 + if ( sasl_client_init( NULL ) != SASL_OK ) { + fprintf(stderr, "SASL library initialization failed!\n"); + exit(-1); + } +#endif + SMTPProtocol slave( argv[2], argv[3], qstricmp( argv[1], "smtps" ) == 0 ); + slave.dispatchLoop(); +#ifdef HAVE_LIBSASL2 + sasl_done(); +#endif + return 0; +} + +SMTPProtocol::SMTPProtocol(const QCString & pool, const QCString & app, + bool useSSL) +: TCPSlaveBase(useSSL ? 465 : 25, + useSSL ? "smtps" : "smtp", + pool, app, useSSL), + m_iOldPort(0), + m_opened(false) +{ + //kdDebug(7112) << "SMTPProtocol::SMTPProtocol" << endl; + mPendingCommandQueue.setAutoDelete( true ); + mSentCommandQueue.setAutoDelete( true ); +} + +unsigned int SMTPProtocol::sendBufferSize() const { + // ### how much is eaten by SSL/TLS overhead? + const int fd = fileno( fp ); + int value = -1; + kde_socklen_t len = sizeof(value); + if ( fd < 0 || ::getsockopt( fd, SOL_SOCKET, SO_SNDBUF, (char*)&value, &len ) ) + value = 1024; // let's be conservative + kdDebug(7112) << "send buffer size seems to be " << value << " octets." << endl; + return value > 0 ? value : 1024 ; +} + +SMTPProtocol::~SMTPProtocol() { + //kdDebug(7112) << "SMTPProtocol::~SMTPProtocol" << endl; + smtp_close(); +} + +void SMTPProtocol::openConnection() { + if ( smtp_open() ) + connected(); + else + closeConnection(); +} + +void SMTPProtocol::closeConnection() { + smtp_close(); +} + +void SMTPProtocol::special( const QByteArray & aData ) { + QDataStream s( aData, IO_ReadOnly ); + int what; + s >> what; + if ( what == 'c' ) { + infoMessage( createSpecialResponse() ); +#ifndef NDEBUG + kdDebug(7112) << "special('c') returns \"" << createSpecialResponse() << "\"" << endl; +#endif + } else if ( what == 'N' ) { + if ( !execute( Command::NOOP ) ) + return; + } else { + error( KIO::ERR_INTERNAL, + i18n("The application sent an invalid request.") ); + return; + } + finished(); +} + + +// Usage: smtp://smtphost:port/send?to=user@host.com&subject=blah +// If smtphost is the name of a profile, it'll use the information +// provided by that profile. If it's not a profile name, it'll use it as +// nature intended. +// One can also specify in the query: +// headers=0 (turns off header generation) +// to=emailaddress +// cc=emailaddress +// bcc=emailaddress +// subject=text +// profile=text (this will override the "host" setting) +// hostname=text (used in the HELO) +// body={7bit,8bit} (default: 7bit; 8bit activates the use of the 8BITMIME SMTP extension) +void SMTPProtocol::put(const KURL & url, int /*permissions */ , + bool /*overwrite */ , bool /*resume */ ) +{ + Request request = Request::fromURL( url ); // parse settings from URL's query + + KEMailSettings mset; + KURL open_url = url; + if ( !request.hasProfile() ) { + //kdDebug(7112) << "kio_smtp: Profile is null" << endl; + bool hasProfile = mset.profiles().contains( open_url.host() ); + if ( hasProfile ) { + mset.setProfile(open_url.host()); + open_url.setHost(mset.getSetting(KEMailSettings::OutServer)); + m_sUser = mset.getSetting(KEMailSettings::OutServerLogin); + m_sPass = mset.getSetting(KEMailSettings::OutServerPass); + + if (m_sUser.isEmpty()) + m_sUser = QString::null; + if (m_sPass.isEmpty()) + m_sPass = QString::null; + open_url.setUser(m_sUser); + open_url.setPass(m_sPass); + m_sServer = open_url.host(); + m_iPort = open_url.port(); + } + else { + mset.setProfile(mset.defaultProfileName()); + } + } + else { + mset.setProfile( request.profileName() ); + } + + // Check KEMailSettings to see if we've specified an E-Mail address + // if that worked, check to see if we've specified a real name + // and then format accordingly (either: emailaddress@host.com or + // Real Name <emailaddress@host.com>) + if ( !request.hasFromAddress() ) { + const QString from = mset.getSetting( KEMailSettings::EmailAddress ); + if ( !from.isNull() ) + request.setFromAddress( from ); + else if ( request.emitHeaders() ) { + error(KIO::ERR_NO_CONTENT, i18n("The sender address is missing.")); + return; + } + } + + if ( !smtp_open( request.heloHostname() ) ) + { + error(KIO::ERR_SERVICE_NOT_AVAILABLE, + i18n("SMTPProtocol::smtp_open failed (%1)") // ### better error message? + .arg(open_url.path())); + return; + } + + if ( request.is8BitBody() + && !haveCapability("8BITMIME") && metaData("8bitmime") != "on" ) { + error( KIO::ERR_SERVICE_NOT_AVAILABLE, + i18n("Your server does not support sending of 8-bit messages.\n" + "Please use base64 or quoted-printable encoding.") ); + return; + } + + queueCommand( new MailFromCommand( this, request.fromAddress().latin1(), + request.is8BitBody(), request.size() ) ); + + // Loop through our To and CC recipients, and send the proper + // SMTP commands, for the benefit of the server. + QStringList recipients = request.recipients(); + for ( QStringList::const_iterator it = recipients.begin() ; it != recipients.end() ; ++it ) + queueCommand( new RcptToCommand( this, (*it).latin1() ) ); + + queueCommand( Command::DATA ); + queueCommand( new TransferCommand( this, request.headerFields( mset.getSetting( KEMailSettings::RealName ) ) ) ); + + TransactionState ts; + if ( !executeQueuedCommands( &ts ) ) { + if ( ts.errorCode() ) + error( ts.errorCode(), ts.errorMessage() ); + } else + finished(); +} + + +void SMTPProtocol::setHost(const QString & host, int port, + const QString & user, const QString & pass) +{ + m_sServer = host; + m_iPort = port; + m_sUser = user; + m_sPass = pass; +} + +bool SMTPProtocol::sendCommandLine( const QCString & cmdline ) { + //kdDebug( cmdline.length() < 4096, 7112) << "C: " << cmdline.data(); + //kdDebug( cmdline.length() >= 4096, 7112) << "C: <" << cmdline.length() << " bytes>" << endl; + kdDebug( 7112) << "C: <" << cmdline.length() << " bytes>" << endl; + ssize_t cmdline_len = cmdline.length(); + if ( write( cmdline.data(), cmdline_len ) != cmdline_len ) { + error( KIO::ERR_COULD_NOT_WRITE, m_sServer ); + return false; + } + return true; +} + +Response SMTPProtocol::getResponse( bool * ok ) { + + if ( ok ) + *ok = false; + + Response response; + char buf[2048]; + + int recv_len = 0; + do { + // wait for data... + if ( !waitForResponse( 600 ) ) { + error( KIO::ERR_SERVER_TIMEOUT, m_sServer ); + return response; + } + + // ...read data... + recv_len = readLine( buf, sizeof(buf) - 1 ); + if ( recv_len < 1 && !isConnectionValid() ) { + error( KIO::ERR_CONNECTION_BROKEN, m_sServer ); + return response; + } + + kdDebug(7112) << "S: " << QCString( buf, recv_len + 1 ).data(); + // ...and parse lines... + response.parseLine( buf, recv_len ); + + // ...until the response is complete or the parser is so confused + // that it doesn't think a RSET would help anymore: + } while ( !response.isComplete() && response.isWellFormed() ); + + if ( !response.isValid() ) { + error( KIO::ERR_NO_CONTENT, i18n("Invalid SMTP response (%1) received.").arg(response.code()) ); + return response; + } + + if ( ok ) + *ok = true; + + return response; +} + +bool SMTPProtocol::executeQueuedCommands( TransactionState * ts ) { + assert( ts ); + + kdDebug( canPipelineCommands(), 7112 ) << "using pipelining" << endl; + + while( !mPendingCommandQueue.isEmpty() ) { + QCString cmdline = collectPipelineCommands( ts ); + if ( ts->failedFatally() ) { + smtp_close( false ); // _hard_ shutdown + return false; + } + if ( ts->failed() ) + break; + if ( cmdline.isEmpty() ) + continue; + if ( !sendCommandLine( cmdline ) || + !batchProcessResponses( ts ) || + ts->failedFatally() ) { + smtp_close( false ); // _hard_ shutdown + return false; + } + } + + if ( ts->failed() ) { + if ( !execute( Command::RSET ) ) + smtp_close( false ); + return false; + } + return true; +} + +QCString SMTPProtocol::collectPipelineCommands( TransactionState * ts ) { + assert( ts ); + + QCString cmdLine; + unsigned int cmdLine_len = 0; + + while ( mPendingCommandQueue.head() ) { + + Command * cmd = mPendingCommandQueue.head(); + + if ( cmd->doNotExecute( ts ) ) { + delete mPendingCommandQueue.dequeue(); + if ( cmdLine_len ) + break; + else + continue; + } + + if ( cmdLine_len && cmd->mustBeFirstInPipeline() ) + break; + + if ( cmdLine_len && !canPipelineCommands() ) + break; + + while ( !cmd->isComplete() && !cmd->needsResponse() ) { + const QCString currentCmdLine = cmd->nextCommandLine( ts ); + if ( ts->failedFatally() ) + return cmdLine; + const unsigned int currentCmdLine_len = currentCmdLine.length(); + + if ( cmdLine_len && cmdLine_len + currentCmdLine_len > sendBufferSize() ) { + // must all fit into the send buffer, else connection deadlocks, + // but we need to have at least _one_ command to send + cmd->ungetCommandLine( currentCmdLine, ts ); + return cmdLine; + } + cmdLine_len += currentCmdLine_len; + cmdLine += currentCmdLine; + } + + mSentCommandQueue.enqueue( mPendingCommandQueue.dequeue() ); + + if ( cmd->mustBeLastInPipeline() ) + break; + } + + return cmdLine; +} + +bool SMTPProtocol::batchProcessResponses( TransactionState * ts ) { + assert( ts ); + + while ( !mSentCommandQueue.isEmpty() ) { + + Command * cmd = mSentCommandQueue.head(); + assert( cmd->isComplete() ); + + bool ok = false; + Response r = getResponse( &ok ); + if ( !ok ) + return false; + cmd->processResponse( r, ts ); + if ( ts->failedFatally() ) + return false; + + mSentCommandQueue.remove(); + } + + return true; +} + +void SMTPProtocol::queueCommand( int type ) { + queueCommand( Command::createSimpleCommand( type, this ) ); +} + +bool SMTPProtocol::execute( int type, TransactionState * ts ) { + auto_ptr<Command> cmd( Command::createSimpleCommand( type, this ) ); + kdFatal( !cmd.get(), 7112 ) << "Command::createSimpleCommand( " << type << " ) returned null!" << endl; + return execute( cmd.get(), ts ); +} + +// ### fold into pipelining engine? How? (execute() is often called +// ### when command queues are _not_ empty!) +bool SMTPProtocol::execute( Command * cmd, TransactionState * ts ) +{ + kdFatal( !cmd, 7112 ) << "SMTPProtocol::execute() called with no command to run!" << endl; + + if (!cmd) + return false; + + if ( cmd->doNotExecute( ts ) ) + return true; + + do { + while ( !cmd->isComplete() && !cmd->needsResponse() ) { + const QCString cmdLine = cmd->nextCommandLine( ts ); + if ( ts && ts->failedFatally() ) { + smtp_close( false ); + return false; + } + if ( cmdLine.isEmpty() ) + continue; + if ( !sendCommandLine( cmdLine ) ) { + smtp_close( false ); + return false; + } + } + + bool ok = false; + Response r = getResponse( &ok ); + if ( !ok ) { + smtp_close( false ); + return false; + } + if ( !cmd->processResponse( r, ts ) ) { + if ( ts && ts->failedFatally() || + cmd->closeConnectionOnError() || + !execute( Command::RSET ) ) + smtp_close( false ); + return false; + } + } while ( !cmd->isComplete() ); + + return true; +} + +bool SMTPProtocol::smtp_open(const QString& fakeHostname) +{ + if (m_opened && + m_iOldPort == port(m_iPort) && + m_sOldServer == m_sServer && + m_sOldUser == m_sUser && + (fakeHostname.isNull() || m_hostname == fakeHostname)) + return true; + + smtp_close(); + if (!connectToHost(m_sServer, m_iPort)) + return false; // connectToHost has already send an error message. + m_opened = true; + + bool ok = false; + Response greeting = getResponse( &ok ); + if ( !ok || !greeting.isOk() ) + { + if ( ok ) + error( KIO::ERR_COULD_NOT_LOGIN, + i18n("The server did not accept the connection.\n" + "%1").arg( greeting.errorMessage() ) ); + smtp_close(); + return false; + } + + if (!fakeHostname.isNull()) + { + m_hostname = fakeHostname; + } + else + { + QString tmpPort; + KSocketAddress* addr = KExtendedSocket::localAddress(m_iSock); + // perform name lookup. NI_NAMEREQD means: don't return a numeric + // value (we need to know when we get have the IP address, so we + // can enclose it in sqaure brackets (domain-literal). Failure to + // do so is normally harmless with IPv4, but fails for IPv6: + if (KExtendedSocket::resolve(addr, m_hostname, tmpPort, NI_NAMEREQD) != 0) + // FQDN resolution failed + // use the IP address as domain-literal + m_hostname = '[' + addr->nodeName() + ']'; + delete addr; + + if(m_hostname.isEmpty()) + { + m_hostname = "localhost.invalid"; + } + } + + EHLOCommand ehloCmdPreTLS( this, m_hostname ); + if ( !execute( &ehloCmdPreTLS ) ) { + smtp_close(); + return false; + } + + if ( ( haveCapability("STARTTLS") && canUseTLS() && metaData("tls") != "off" ) + || metaData("tls") == "on" ) { + // For now we're gonna force it on. + + if ( execute( Command::STARTTLS ) ) { + + // re-issue EHLO to refresh the capability list (could be have + // been faked before TLS was enabled): + EHLOCommand ehloCmdPostTLS( this, m_hostname ); + if ( !execute( &ehloCmdPostTLS ) ) { + smtp_close(); + return false; + } + } + } + // Now we try and login + if (!authenticate()) { + smtp_close(); + return false; + } + + m_iOldPort = m_iPort; + m_sOldServer = m_sServer; + m_sOldUser = m_sUser; + m_sOldPass = m_sPass; + + return true; +} + +bool SMTPProtocol::authenticate() +{ + // return with success if the server doesn't support SMTP-AUTH or an user + // name is not specified and metadata doesn't tell us to force it. + if ( (m_sUser.isEmpty() || !haveCapability( "AUTH" )) && + metaData( "sasl" ).isEmpty() ) return true; + + KIO::AuthInfo authInfo; + authInfo.username = m_sUser; + authInfo.password = m_sPass; + authInfo.prompt = i18n("Username and password for your SMTP account:"); + + QStringList strList; + + if (!metaData("sasl").isEmpty()) + strList.append(metaData("sasl").latin1()); + else + strList = mCapabilities.saslMethodsQSL(); + + AuthCommand authCmd( this, strList.join(" ").latin1(), m_sServer, authInfo ); + bool ret = execute( &authCmd ); + m_sUser = authInfo.username; + m_sPass = authInfo.password; + return ret; +} + +void SMTPProtocol::parseFeatures( const Response & ehloResponse ) { + mCapabilities = Capabilities::fromResponse( ehloResponse ); + + QString category = usingTLS() ? "TLS" : usingSSL() ? "SSL" : "PLAIN" ; + setMetaData( category + " AUTH METHODS", mCapabilities.authMethodMetaData() ); + setMetaData( category + " CAPABILITIES", mCapabilities.asMetaDataString() ); +#ifndef NDEBUG + kdDebug(7112) << "parseFeatures() " << category << " AUTH METHODS:" + << '\n' + mCapabilities.authMethodMetaData() << endl + << "parseFeatures() " << category << " CAPABILITIES:" + << '\n' + mCapabilities.asMetaDataString() << endl; +#endif +} + +void SMTPProtocol::smtp_close( bool nice ) { + if (!m_opened) // We're already closed + return; + + if ( nice ) + execute( Command::QUIT ); + kdDebug( 7112 ) << "closing connection" << endl; + closeDescriptor(); + m_sOldServer = QString::null; + m_sOldUser = QString::null; + m_sOldPass = QString::null; + + mCapabilities.clear(); + mPendingCommandQueue.clear(); + mSentCommandQueue.clear(); + + m_opened = false; +} + +void SMTPProtocol::stat(const KURL & url) +{ + QString path = url.path(); + error(KIO::ERR_DOES_NOT_EXIST, url.path()); +} + diff --git a/kioslave/smtp/smtp.h b/kioslave/smtp/smtp.h new file mode 100644 index 000000000..571375bd4 --- /dev/null +++ b/kioslave/smtp/smtp.h @@ -0,0 +1,146 @@ +/* -*- c++ -*- + * Copyright (c) 2000, 2001 Alex Zepeda <zipzippy@sonic.net> + * Copyright (c) 2001 Michael Häckel <Michael@Haeckel.Net> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + */ + +#ifndef _SMTP_H +#define _SMTP_H + +#include <kio/tcpslavebase.h> + +#include "capabilities.h" + +#include <qstring.h> +#include <qptrqueue.h> + +class KURL; +class QCString; +template <typename T> class QMemArray; +typedef QMemArray<char> QByteArray; + +namespace KioSMTP { + class Response; + class TransactionState; + class Command; +} + +class SMTPProtocol : public KIO::TCPSlaveBase { + friend class KioSMTP::Command; +public: + SMTPProtocol(const QCString & pool, const QCString & app, bool useSSL); + virtual ~ SMTPProtocol(); + + virtual void setHost(const QString & host, int port, + const QString & user, const QString & pass); + + virtual void special(const QByteArray & aData); + virtual void put(const KURL & url, int permissions, bool overwrite, + bool resume); + virtual void stat(const KURL & url); + virtual void openConnection(); + virtual void closeConnection(); + +protected: + + bool smtp_open(const QString& fakeHostname = QString::null); + + /** Closes the connection. If @p nice is true (default), then QUIT + is sent and it's reponse waited for. */ + void smtp_close( bool nice=true ); + + /** Execute command @p cmd */ + bool execute( KioSMTP::Command * cmd, KioSMTP::TransactionState * ts=0 ); + /** Execute a command of type @p type */ + bool execute( int type, KioSMTP::TransactionState * ts=0 ); + /** Execute the queued commands. If something goes horribly wrong + (sending command oline fails, getting response fails or some + command raises the failedFatally() flag in @p ts, shuts down the + connection with <code>smtp_close( false )</code>. If The + transaction fails gracefully (<code>ts->failed()</code> is + true), issues a RSET command. + + @return true if transaction succeeded, false otherwise. + **/ + bool executeQueuedCommands( KioSMTP::TransactionState * ts ); + + /** Parse a single response from the server. Single- vs. multiline + responses are correctly detected. + + @param ok if not 0, returns whether response parsing was + successful. Don't confuse this with negative responses + (e.g. 5xx), which you can check for using + @ref Response::isNegative() + @return the @ref Response object representing the server response. + **/ + KioSMTP::Response getResponse( bool * ok ); + + bool authenticate(); + void parseFeatures( const KioSMTP::Response & ehloResponse ); + + bool sendCommandLine( const QCString & cmd ); + QCString collectPipelineCommands( KioSMTP::TransactionState * ts ); + bool batchProcessResponses( KioSMTP::TransactionState * ts ); + + /** This is a pure convenience wrapper around + @ref KioSMTP::Capabilities::have() */ + bool haveCapability( const char * cap ) const { + return mCapabilities.have( cap ); + } + + /** @return true is pipelining is available and allowed by metadata */ + bool canPipelineCommands() const { + return haveCapability("PIPELINING") && metaData("pipelining") != "off" ; + } + + /** Wrapper around getsockopt(..., SO_SNDBUF,...) */ + unsigned int sendBufferSize() const; + + /** This is a pure convenience wrapper around + @ref KioSMTP::Capabilities::createSpecialResponse */ + QString createSpecialResponse() const { + return mCapabilities.createSpecialResponse( usingTLS() || haveCapability( "STARTTLS" ) ); + } + + void queueCommand( KioSMTP::Command * command ) { + mPendingCommandQueue.enqueue( command ); + } + void queueCommand( int type ); + + unsigned short m_iOldPort; + bool m_opened; + QString m_sServer, m_sOldServer; + QString m_sUser, m_sOldUser; + QString m_sPass, m_sOldPass; + QString m_hostname; + + KioSMTP::Capabilities mCapabilities; + + typedef QPtrQueue<KioSMTP::Command> CommandQueue; + CommandQueue mPendingCommandQueue; + CommandQueue mSentCommandQueue; +}; + +#endif // _SMTP_H diff --git a/kioslave/smtp/smtp.protocol b/kioslave/smtp/smtp.protocol new file mode 100644 index 000000000..e01be9c7f --- /dev/null +++ b/kioslave/smtp/smtp.protocol @@ -0,0 +1,16 @@ +[Protocol] +exec=kio_smtp +protocol=smtp +Capabilities=SASL +input=none +output=filesystem +listing=Name,Type,Size +reading=false +writing=true +deleting=false +source=true +makedir=false +linking=false +moving=false +DocPath=kioslave/smtp.html +Icon=folder_outbox diff --git a/kioslave/smtp/smtps.protocol b/kioslave/smtp/smtps.protocol new file mode 100644 index 000000000..2087845f2 --- /dev/null +++ b/kioslave/smtp/smtps.protocol @@ -0,0 +1,15 @@ +[Protocol] +exec=kio_smtp +protocol=smtps +Capabilities=SASL +input=none +output=filesystem +listing=Name,Type,Size +reading=false +writing=true +deleting=false +source=true +makedir=false +linking=false +moving=false +Icon=folder_outbox diff --git a/kioslave/smtp/test_commands.cc b/kioslave/smtp/test_commands.cc new file mode 100644 index 000000000..a6969490f --- /dev/null +++ b/kioslave/smtp/test_commands.cc @@ -0,0 +1,728 @@ +#include <kio/global.h> +#include <kdebug.h> + +#include <qstring.h> +#include <qcstring.h> +#include <qstringlist.h> + +//#include <iostream> +//using std::cout; +//using std::endl; + +namespace KioSMTP { + class Response; +}; + +// fake +class SMTPProtocol { +public: + SMTPProtocol() { clear(); } + + // + // public members to control the API emulation below: + // + int startTLSReturnCode; + bool usesSSL; + bool usesTLS; + int lastErrorCode; + QString lastErrorMessage; + int lastMessageBoxCode; + QString lastMessageBoxText; + QByteArray nextData; + int nextDataReturnCode; + QStringList caps; + KIO::MetaData metadata; + + void clear() { + startTLSReturnCode = 1; + usesSSL = usesTLS = false; + lastErrorCode = lastMessageBoxCode = 0; + lastErrorMessage = lastMessageBoxText = QString::null; + nextData.resize( 0 ); + nextDataReturnCode = -1; + caps.clear(); + metadata.clear(); + } + + // + // emulated API: + // + void parseFeatures( const KioSMTP::Response & ) { /* noop */ } + int startTLS() { + if ( startTLSReturnCode == 1 ) + usesTLS = true; + return startTLSReturnCode; + } + bool usingSSL() const { return usesSSL; } + bool usingTLS() const { return usesTLS; } + bool haveCapability( const char * cap ) const { return caps.contains( cap ); } + void error( int id, const QString & msg ) { + lastErrorCode = id; + lastErrorMessage = msg; + } + void messageBox( int id, const QString & msg, const QString & ) { + lastMessageBoxCode = id; + lastMessageBoxText = msg; + } + void dataReq() { /* noop */ } + int readData( QByteArray & ba ) { ba = nextData; return nextDataReturnCode; } + QString metaData( const QString & key ) const { return metadata[key]; } + +}; + +#define _SMTP_H + +#define KIOSMTP_COMPARATORS // for TransactionState::operator== +#include "command.h" +#include "response.h" +#include "transactionstate.h" + +#include <assert.h> + +using namespace KioSMTP; + +static const char * foobarbaz = ".Foo bar baz"; +static const unsigned int foobarbaz_len = qstrlen( foobarbaz ); + +static const char * foobarbaz_dotstuffed = "..Foo bar baz"; +static const unsigned int foobarbaz_dotstuffed_len = qstrlen( foobarbaz_dotstuffed ); + +static const char * foobarbaz_lf = ".Foo bar baz\n"; +static const unsigned int foobarbaz_lf_len = qstrlen( foobarbaz_lf ); + +static const char * foobarbaz_crlf = "..Foo bar baz\r\n"; +static const unsigned int foobarbaz_crlf_len = qstrlen( foobarbaz_crlf ); + +static void checkSuccessfulTransferCommand( bool, bool, bool, bool, bool ); + +int main( int, char** ) { + + // FIXME: Port this to new API. +#if 0 + SMTPProtocol smtp; + Response r; + TransactionState ts, ts2; + + // + // EHLO / HELO + // + + smtp.clear(); + EHLOCommand ehlo( &smtp, "mail.example.com" ); + // flags + assert( ehlo.closeConnectionOnError() ); + assert( ehlo.mustBeLastInPipeline() ); + assert( !ehlo.mustBeFirstInPipeline() ); + + // initial state + assert( !ehlo.isComplete() ); + assert( !ehlo.doNotExecute( 0 ) ); + assert( !ehlo.needsResponse() ); + + // dynamics 1: EHLO succeeds + assert( ehlo.nextCommandLine( 0 ) == "EHLO mail.example.com\r\n" ); + assert( !ehlo.isComplete() ); // EHLO may fail and we then try HELO + assert( ehlo.needsResponse() ); + r.clear(); + r.parseLine( "250-mail.example.net\r\n" ); + r.parseLine( "250-PIPELINING\r\n" ); + r.parseLine( "250 8BITMIME\r\n" ); + assert( ehlo.processResponse( r, 0 ) == true ); + assert( ehlo.isComplete() ); + assert( !ehlo.needsResponse() ); + assert( smtp.lastErrorCode == 0 ); + assert( smtp.lastErrorMessage.isNull() ); + + // dynamics 2: EHLO fails with "unknown command" + smtp.clear(); + EHLOCommand ehlo2( &smtp, "mail.example.com" ); + ehlo2.nextCommandLine( 0 ); + r.clear(); + r.parseLine( "500 unknown command\r\n" ); + assert( ehlo2.processResponse( r, 0 ) == true ); + assert( !ehlo2.isComplete() ); + assert( !ehlo2.needsResponse() ); + assert( ehlo2.nextCommandLine( 0 ) == "HELO mail.example.com\r\n" ); + assert( ehlo2.isComplete() ); + assert( ehlo2.needsResponse() ); + r.clear(); + r.parseLine( "250 mail.example.net\r\n" ); + assert( ehlo2.processResponse( r, 0 ) == true ); + assert( !ehlo2.needsResponse() ); + assert( smtp.lastErrorCode == 0 ); + assert( smtp.lastErrorMessage.isNull() ); + + // dynamics 3: EHLO fails with unknown response code + smtp.clear(); + EHLOCommand ehlo3( &smtp, "mail.example.com" ); + ehlo3.nextCommandLine( 0 ); + r.clear(); + r.parseLine( "545 you don't know me\r\n" ); + assert( ehlo3.processResponse( r, 0 ) == false ); + assert( ehlo3.isComplete() ); + assert( !ehlo3.needsResponse() ); + assert( smtp.lastErrorCode == KIO::ERR_UNKNOWN ); + + // dynamics 4: EHLO _and_ HELO fail with "command unknown" + smtp.clear(); + EHLOCommand ehlo4( &smtp, "mail.example.com" ); + ehlo4.nextCommandLine( 0 ); + r.clear(); + r.parseLine( "500 unknown command\r\n" ); + ehlo4.processResponse( r, 0 ); + ehlo4.nextCommandLine( 0 ); + r.clear(); + r.parseLine( "500 unknown command\r\n" ); + assert( ehlo4.processResponse( r, 0 ) == false ); + assert( ehlo4.isComplete() ); + assert( !ehlo4.needsResponse() ); + assert( smtp.lastErrorCode == KIO::ERR_INTERNAL_SERVER ); + + // + // STARTTLS + // + + smtp.clear(); + StartTLSCommand tls( &smtp ); + // flags + assert( tls.closeConnectionOnError() ); + assert( tls.mustBeLastInPipeline() ); + assert( !tls.mustBeFirstInPipeline() ); + + // initial state + assert( !tls.isComplete() ); + assert( !tls.doNotExecute( 0 ) ); + assert( !tls.needsResponse() ); + + // dynamics 1: ok from server, TLS negotiation successful + ts.clear(); + ts2 = ts; + assert( tls.nextCommandLine( &ts ) == "STARTTLS\r\n" ); + assert( ts == ts2 ); + assert( tls.isComplete() ); + assert( tls.needsResponse() ); + r.clear(); + r.parseLine( "220 Go ahead" ); + smtp.startTLSReturnCode = 1; + assert( tls.processResponse( r, &ts ) == true ); + assert( !tls.needsResponse() ); + assert( smtp.lastErrorCode == 0 ); + + // dynamics 2: NAK from server + smtp.clear(); + StartTLSCommand tls2( &smtp ); + ts.clear(); + tls2.nextCommandLine( &ts ); + r.clear(); + r.parseLine( "454 TLS temporarily disabled" ); + smtp.startTLSReturnCode = 1; + assert( tls2.processResponse( r, &ts ) == false ); + assert( !tls2.needsResponse() ); + assert( smtp.lastErrorCode == KIO::ERR_SERVICE_NOT_AVAILABLE ); + + // dynamics 3: ok from server, TLS negotiation unsuccessful + smtp.clear(); + StartTLSCommand tls3( &smtp ); + ts.clear(); + tls3.nextCommandLine( &ts ); + r.clear(); + r.parseLine( "220 Go ahead" ); + smtp.startTLSReturnCode = -1; + assert( tls.processResponse( r, &ts ) == false ); + assert( !tls.needsResponse() ); + + // + // AUTH + // + + smtp.clear(); + QStrIList mechs; + mechs.append( "PLAIN" ); + smtp.metadata["sasl"] = "PLAIN"; + AuthCommand auth( &smtp, mechs, "user", "pass" ); + // flags + assert( auth.closeConnectionOnError() ); + assert( auth.mustBeLastInPipeline() ); + assert( !auth.mustBeFirstInPipeline() ); + + // initial state + assert( !auth.isComplete() ); + assert( !auth.doNotExecute( 0 ) ); + assert( !auth.needsResponse() ); + + // dynamics 1: TLS, so AUTH should include initial-response: + smtp.usesTLS = true; + ts.clear(); + ts2 = ts; + assert( auth.nextCommandLine( &ts ) == "AUTH PLAIN dXNlcgB1c2VyAHBhc3M=\r\n" ); + assert( auth.isComplete() ); + assert( auth.needsResponse() ); + assert( ts == ts2 ); + r.clear(); + r.parseLine( "250 OK" ); + + // dynamics 2: No TLS, so AUTH should not include initial-response: + smtp.clear(); + smtp.metadata["sasl"] = "PLAIN"; + smtp.usesTLS = false; + AuthCommand auth2( &smtp, mechs, "user", "pass" ); + ts.clear(); + assert( auth2.nextCommandLine( &ts ) == "AUTH PLAIN\r\n" ); + assert( !auth2.isComplete() ); + assert( auth2.needsResponse() ); + r.clear(); + r.parseLine( "334 Go on" ); + assert( auth2.processResponse( r, &ts ) == true ); + assert( auth2.nextCommandLine( &ts ) == "dXNlcgB1c2VyAHBhc3M=\r\n" ); + assert( auth2.isComplete() ); + assert( auth2.needsResponse() ); + + // dynamics 3: LOGIN + smtp.clear(); + smtp.metadata["sasl"] = "LOGIN"; + mechs.clear(); + mechs.append( "LOGIN" ); + AuthCommand auth3( &smtp, mechs, "user", "pass" ); + ts.clear(); + ts2 = ts; + assert( auth3.nextCommandLine( &ts ) == "AUTH LOGIN\r\n" ); + assert( !auth3.isComplete() ); + assert( auth3.needsResponse() ); + r.clear(); + r.parseLine( "334 VXNlcm5hbWU6" ); + assert( auth3.processResponse( r, &ts ) == true ); + assert( !auth3.needsResponse() ); + assert( auth3.nextCommandLine( &ts ) == "dXNlcg==\r\n" ); + assert( !auth3.isComplete() ); + assert( auth3.needsResponse() ); + r.clear(); + r.parseLine( "334 go on" ); + assert( auth3.processResponse( r, &ts ) == true ); + assert( !auth3.needsResponse() ); + assert( auth3.nextCommandLine( &ts ) == "cGFzcw==\r\n" ); + assert( auth3.isComplete() ); + assert( auth3.needsResponse() ); + r.clear(); + r.parseLine( "250 OK" ); + assert( auth3.processResponse( r, &ts ) == true ); + assert( !auth3.needsResponse() ); + assert( !smtp.lastErrorCode ); + assert( ts == ts2 ); + + // + // MAIL FROM: + // + + smtp.clear(); + MailFromCommand mail( &smtp, "joe@user.org" ); + // flags + assert( !mail.closeConnectionOnError() ); + assert( !mail.mustBeLastInPipeline() ); + assert( !mail.mustBeFirstInPipeline() ); + + // initial state + assert( !mail.isComplete() ); + assert( !mail.doNotExecute( 0 ) ); + assert( !mail.needsResponse() ); + + // dynamics: success, no size, no 8bit + ts.clear(); + ts2 = ts; + assert( mail.nextCommandLine( &ts ) == "MAIL FROM:<joe@user.org>\r\n" ); + assert( ts2 == ts ); + assert( mail.isComplete() ); + assert( mail.needsResponse() ); + r.clear(); + r.parseLine( "250 Ok" ); + assert( mail.processResponse( r, &ts ) == true ); + assert( !mail.needsResponse() ); + assert( ts == ts2 ); + assert( smtp.lastErrorCode == 0 ); + + // dynamics: success, size, 8bit, but no SIZE, 8BITMIME caps + smtp.clear(); + MailFromCommand mail2( &smtp, "joe@user.org", true, 500 ); + ts.clear(); + ts2 = ts; + assert( mail2.nextCommandLine( &ts ) == "MAIL FROM:<joe@user.org>\r\n" ); + assert( ts == ts2 ); + + // dynamics: success, size, 8bit, SIZE, 8BITMIME caps + smtp.clear(); + MailFromCommand mail3( &smtp, "joe@user.org", true, 500 ); + ts.clear(); + ts2 = ts; + smtp.caps << "SIZE" << "8BITMIME" ; + assert( mail3.nextCommandLine( &ts ) == "MAIL FROM:<joe@user.org> BODY=8BITMIME SIZE=500\r\n" ); + assert( ts == ts2 ); + + // dynamics: failure + smtp.clear(); + MailFromCommand mail4( &smtp, "joe@user.org" ); + ts.clear(); + mail4.nextCommandLine( &ts ); + r.clear(); + r.parseLine( "503 Bad sequence of commands" ); + assert( mail4.processResponse( r, &ts ) == false ); + assert( mail4.isComplete() ); + assert( !mail4.needsResponse() ); + assert( ts.failed() ); + assert( !ts.failedFatally() ); + assert( smtp.lastErrorCode == 0 ); + + // + // RCPT TO: + // + + smtp.clear(); + RcptToCommand rcpt( &smtp, "joe@user.org" ); + // flags + assert( !rcpt.closeConnectionOnError() ); + assert( !rcpt.mustBeLastInPipeline() ); + assert( !rcpt.mustBeFirstInPipeline() ); + + // initial state + assert( !rcpt.isComplete() ); + assert( !rcpt.doNotExecute( 0 ) ); + assert( !rcpt.needsResponse() ); + + // dynamics: success + ts.clear(); + ts2 = ts; + assert( rcpt.nextCommandLine( &ts ) == "RCPT TO:<joe@user.org>\r\n" ); + assert( ts == ts2 ); + assert( rcpt.isComplete() ); + assert( rcpt.needsResponse() ); + r.clear(); + r.parseLine( "250 Ok" ); + assert( rcpt.processResponse( r, &ts ) == true ); + assert( !rcpt.needsResponse() ); + assert( ts.atLeastOneRecipientWasAccepted() ); + assert( !ts.haveRejectedRecipients() ); + assert( !ts.failed() ); + assert( !ts.failedFatally() ); + assert( smtp.lastErrorCode == 0 ); + + // dynamics: failure + smtp.clear(); + RcptToCommand rcpt2( &smtp, "joe@user.org" ); + ts.clear(); + rcpt2.nextCommandLine( &ts ); + r.clear(); + r.parseLine( "530 5.7.1 Relaying not allowed!" ); + assert( rcpt2.processResponse( r, &ts ) == false ); + assert( rcpt2.isComplete() ); + assert( !rcpt2.needsResponse() ); + assert( !ts.atLeastOneRecipientWasAccepted() ); + assert( ts.haveRejectedRecipients() ); + assert( ts.rejectedRecipients().count() == 1 ); + assert( ts.rejectedRecipients().front().recipient == "joe@user.org" ); + assert( ts.failed() ); + assert( !ts.failedFatally() ); + assert( smtp.lastErrorCode == 0 ); + + // dynamics: success and failure combined + smtp.clear(); + RcptToCommand rcpt3( &smtp, "info@example.com" ); + RcptToCommand rcpt4( &smtp, "halloween@microsoft.com" ); + RcptToCommand rcpt5( &smtp, "joe@user.org" ); + ts.clear(); + rcpt3.nextCommandLine( &ts ); + r.clear(); + r.parseLine( "530 5.7.1 Relaying not allowed!" ); + rcpt3.processResponse( r, &ts ); + + rcpt4.nextCommandLine( &ts ); + r.clear(); + r.parseLine( "250 Ok" ); + rcpt4.processResponse( r, &ts ); + + rcpt5.nextCommandLine( &ts ); + r.clear(); + r.parseLine( "250 Ok" ); + assert( ts.failed() ); + assert( !ts.failedFatally() ); + assert( ts.haveRejectedRecipients() ); + assert( ts.atLeastOneRecipientWasAccepted() ); + assert( smtp.lastErrorCode == 0 ); + + // + // DATA (init) + // + + smtp.clear(); + DataCommand data( &smtp ); + // flags + assert( !data.closeConnectionOnError() ); + assert( data.mustBeLastInPipeline() ); + assert( !data.mustBeFirstInPipeline() ); + + // initial state + assert( !data.isComplete() ); + assert( !data.doNotExecute( 0 ) ); + assert( !data.needsResponse() ); + + // dynamics: success + ts.clear(); + assert( data.nextCommandLine( &ts ) == "DATA\r\n" ); + assert( data.isComplete() ); + assert( data.needsResponse() ); + assert( ts.dataCommandIssued() ); + assert( !ts.dataCommandSucceeded() ); + r.clear(); + r.parseLine( "354 Send data, end in <CR><LF>.<CR><LF>" ); + assert( data.processResponse( r, &ts ) == true ); + assert( !data.needsResponse() ); + assert( ts.dataCommandSucceeded() ); + assert( ts.dataResponse() == r ); + assert( smtp.lastErrorCode == 0 ); + + // dynamics: failure + smtp.clear(); + DataCommand data2( &smtp ); + ts.clear(); + data2.nextCommandLine( &ts ); + r.clear(); + r.parseLine( "551 No valid recipients" ); + assert( data2.processResponse( r, &ts ) == false ); + assert( !data2.needsResponse() ); + assert( !ts.dataCommandSucceeded() ); + assert( ts.dataResponse() == r ); + assert( smtp.lastErrorCode == 0 ); + + // + // DATA (transfer) + // + + TransferCommand xfer( &smtp, 0 ); + // flags + assert( !xfer.closeConnectionOnError() ); + assert( !xfer.mustBeLastInPipeline() ); + assert( xfer.mustBeFirstInPipeline() ); + + // initial state + assert( !xfer.isComplete() ); + assert( !xfer.needsResponse() ); + + // dynamics 1: DATA command failed + ts.clear(); + r.clear(); + r.parseLine( "551 no valid recipients" ); + ts.setDataCommandIssued( true ); + ts.setDataCommandSucceeded( false, r ); + assert( xfer.doNotExecute( &ts ) ); + + // dynamics 2: some recipients rejected, but not all + smtp.clear(); + TransferCommand xfer2( &smtp, 0 ); + ts.clear(); + ts.setRecipientAccepted(); + ts.addRejectedRecipient( "joe@user.org", "No relaying allowed" ); + ts.setDataCommandIssued( true ); + r.clear(); + r.parseLine( "354 go on" ); + ts.setDataCommandSucceeded( true, r ); + // ### will change with allow-partial-delivery option: + assert( xfer.doNotExecute( &ts ) ); + + // successful dynamics with all combinations of: + enum { + EndInLF = 1, + PerformDotStuff = 2, + UngetLast = 4, + Preloading = 8, + Error = 16, + EndOfOptions = 32 + }; + for ( unsigned int i = 0 ; i < EndOfOptions ; ++i ) + checkSuccessfulTransferCommand( i & Error, i & Preloading, i & UngetLast, + i & PerformDotStuff, i & EndInLF ); + + // + // NOOP + // + + smtp.clear(); + NoopCommand noop( &smtp ); + // flags + assert( !noop.closeConnectionOnError() ); + assert( noop.mustBeLastInPipeline() ); + assert( !noop.mustBeFirstInPipeline() ); + + // initial state + assert( !noop.isComplete() ); + assert( !noop.doNotExecute( &ts ) ); + assert( !noop.needsResponse() ); + + // dynamics: success (failure is tested with RSET) + assert( noop.nextCommandLine( 0 ) == "NOOP\r\n" ); + assert( noop.isComplete() ); + assert( noop.needsResponse() ); + r.clear(); + r.parseLine( "250 Ok" ); + assert( noop.processResponse( r, 0 ) == true ); + assert( noop.isComplete() ); + assert( !noop.needsResponse() ); + assert( smtp.lastErrorCode == 0 ); + assert( smtp.lastErrorMessage.isNull() ); + + // + // RSET + // + + smtp.clear(); + RsetCommand rset( &smtp ); + // flags + assert( rset.closeConnectionOnError() ); + assert( !rset.mustBeLastInPipeline() ); + assert( !rset.mustBeFirstInPipeline() ); + + // initial state + assert( !rset.isComplete() ); + assert( !rset.doNotExecute( &ts ) ); + assert( !rset.needsResponse() ); + + // dynamics: failure (success is tested with NOOP/QUIT) + assert( rset.nextCommandLine( 0 ) == "RSET\r\n" ); + assert( rset.isComplete() ); + assert( rset.needsResponse() ); + r.clear(); + r.parseLine( "502 command not implemented" ); + assert( rset.processResponse( r, 0 ) == false ); + assert( rset.isComplete() ); + assert( !rset.needsResponse() ); + assert( smtp.lastErrorCode == 0 ); // an RSET failure isn't worth it, is it? + assert( smtp.lastErrorMessage.isNull() ); + + // + // QUIT + // + + smtp.clear(); + QuitCommand quit( &smtp ); + // flags + assert( quit.closeConnectionOnError() ); + assert( quit.mustBeLastInPipeline() ); + assert( !quit.mustBeFirstInPipeline() ); + + // initial state + assert( !quit.isComplete() ); + assert( !quit.doNotExecute( 0 ) ); + assert( !quit.needsResponse() ); + + // dynamics 1: success + assert( quit.nextCommandLine( 0 ) == "QUIT\r\n" ); + assert( quit.isComplete() ); + assert( quit.needsResponse() ); + r.clear(); + r.parseLine( "221 Goodbye" ); + assert( quit.processResponse( r, 0 ) == true ); + assert( quit.isComplete() ); + assert( !quit.needsResponse() ); + assert( smtp.lastErrorCode == 0 ); + assert( smtp.lastErrorMessage.isNull() ); + + // dynamics 2: success + smtp.clear(); + QuitCommand quit2( &smtp ); + quit2.nextCommandLine( 0 ); + r.clear(); + r.parseLine( "500 unknown command" ); + assert( quit2.processResponse( r, 0 ) == false ); + assert( quit2.isComplete() ); + assert( !quit2.needsResponse() ); + assert( smtp.lastErrorCode == 0 ); // an QUIT failure isn't worth it, is it? + assert( smtp.lastErrorMessage.isNull() ); +#endif + + return 0; +} + +void checkSuccessfulTransferCommand( bool error, bool preload, bool ungetLast, + bool slaveDotStuff, bool mailEndsInNewline ) { + kdDebug() << " ===== checkTransferCommand( " + << error << ", " + << preload << ", " + << ungetLast << ", " + << slaveDotStuff << ", " + << mailEndsInNewline << " ) =====" << endl; + + SMTPProtocol smtp; + if ( slaveDotStuff ) + smtp.metadata["lf2crlf+dotstuff"] = "slave"; + + Response r; + + const char * s_pre = slaveDotStuff ? + mailEndsInNewline ? foobarbaz_lf : foobarbaz + : + mailEndsInNewline ? foobarbaz_crlf : foobarbaz_dotstuffed ; + const unsigned int s_pre_len = qstrlen( s_pre ); + + const char * s_post = mailEndsInNewline ? foobarbaz_crlf : foobarbaz_dotstuffed ; + //const unsigned int s_post_len = qstrlen( s_post ); + + TransferCommand xfer( &smtp, preload ? s_post : 0 ); + + TransactionState ts; + ts.setRecipientAccepted(); + ts.setDataCommandIssued( true ); + r.clear(); + r.parseLine( "354 ok" ); + ts.setDataCommandSucceeded( true, r ); + assert( !xfer.doNotExecute( &ts ) ); + if ( preload ) { + assert( xfer.nextCommandLine( &ts ) == s_post ); + assert( !xfer.isComplete() ); + assert( !xfer.needsResponse() ); + assert( !ts.failed() ); + assert( smtp.lastErrorCode == 0 ); + } + smtp.nextData.duplicate( s_pre, s_pre_len ); + smtp.nextDataReturnCode = s_pre_len; + assert( xfer.nextCommandLine( &ts ) == s_post ); + assert( !xfer.isComplete() ); + assert( !xfer.needsResponse() ); + assert( !ts.failed() ); + assert( smtp.lastErrorCode == 0 ); + smtp.nextData.resize( 0 ); + smtp.nextDataReturnCode = 0; + if ( ungetLast ) { + xfer.ungetCommandLine( xfer.nextCommandLine( &ts ), &ts ); + assert( !xfer.isComplete() ); + assert( !xfer.needsResponse() ); + assert( !ts.complete() ); + smtp.nextDataReturnCode = -1; // double read -> error + } + if ( mailEndsInNewline ) + assert( xfer.nextCommandLine( &ts ) == ".\r\n" ); + else + assert( xfer.nextCommandLine( &ts ) == "\r\n.\r\n" ); + assert( xfer.isComplete() ); + assert( xfer.needsResponse() ); + assert( !ts.complete() ); + assert( !ts.failed() ); + assert( smtp.lastErrorCode == 0 ); + r.clear(); + if ( error ) { + r.parseLine( "552 Exceeded storage allocation" ); + assert( xfer.processResponse( r, &ts ) == false ); + assert( !xfer.needsResponse() ); + assert( ts.complete() ); + assert( ts.failed() ); + assert( smtp.lastErrorCode == KIO::ERR_DISK_FULL ); + } else { + r.parseLine( "250 Message accepted" ); + assert( xfer.processResponse( r, &ts ) == true ); + assert( !xfer.needsResponse() ); + assert( ts.complete() ); + assert( !ts.failed() ); + assert( smtp.lastErrorCode == 0 ); + } +}; + +#define NDEBUG + +#include "command.cc" +#include "response.cc" +#include "transactionstate.cc" diff --git a/kioslave/smtp/test_headergeneration.cc b/kioslave/smtp/test_headergeneration.cc new file mode 100644 index 000000000..bdf8b251f --- /dev/null +++ b/kioslave/smtp/test_headergeneration.cc @@ -0,0 +1,86 @@ +#include "request.h" + +//#include <iostream> + +//using std::cout; +//using std::endl; + +int main( int , char ** ) { + static QCString expected = + "From: mutz@kde.org\r\n" + "Subject: missing subject\r\n" + "To: joe@user.org,\r\n" + "\tvalentine@14th.february.org\r\n" + "Cc: boss@example.com\r\n" + "\n" + "From: Marc Mutz <mutz@kde.org>\r\n" + "Subject: missing subject\r\n" + "To: joe@user.org,\r\n" + "\tvalentine@14th.february.org\r\n" + "Cc: boss@example.com\r\n" + "\n" + "From: \"Mutz, Marc\" <mutz@kde.org>\r\n" + "Subject: missing subject\r\n" + "To: joe@user.org,\r\n" + "\tvalentine@14th.february.org\r\n" + "Cc: boss@example.com\r\n" + "\n" + "From: =?utf-8?b?TWFyYyBNw7Z0eg==?= <mutz@kde.org>\r\n" + "Subject: missing subject\r\n" + "To: joe@user.org,\r\n" + "\tvalentine@14th.february.org\r\n" + "Cc: boss@example.com\r\n" + "\n" + "From: mutz@kde.org\r\n" + "Subject: =?utf-8?b?QmzDtmRlcyBTdWJqZWN0?=\r\n" + "To: joe@user.org,\r\n" + "\tvalentine@14th.february.org\r\n" + "Cc: boss@example.com\r\n" + "\n" + "From: Marc Mutz <mutz@kde.org>\r\n" + "Subject: =?utf-8?b?QmzDtmRlcyBTdWJqZWN0?=\r\n" + "To: joe@user.org,\r\n" + "\tvalentine@14th.february.org\r\n" + "Cc: boss@example.com\r\n" + "\n" + "From: \"Mutz, Marc\" <mutz@kde.org>\r\n" + "Subject: =?utf-8?b?QmzDtmRlcyBTdWJqZWN0?=\r\n" + "To: joe@user.org,\r\n" + "\tvalentine@14th.february.org\r\n" + "Cc: boss@example.com\r\n" + "\n" + "From: =?utf-8?b?TWFyYyBNw7Z0eg==?= <mutz@kde.org>\r\n" + "Subject: =?utf-8?b?QmzDtmRlcyBTdWJqZWN0?=\r\n" + "To: joe@user.org,\r\n" + "\tvalentine@14th.february.org\r\n" + "Cc: boss@example.com\r\n" + "\n"; + + KioSMTP::Request request; + QCString result; + + request.setEmitHeaders( true ); + request.setFromAddress( "mutz@kde.org" ); + request.addTo( "joe@user.org" ); + request.addTo( "valentine@14th.february.org" ); + request.addCc( "boss@example.com" ); + + result += request.headerFields() + '\n'; + result += request.headerFields( "Marc Mutz" ) + '\n'; + result += request.headerFields( "Mutz, Marc" ) + '\n'; + result += request.headerFields( "Marc Mötz" ) + '\n'; + + request.setSubject( "Blödes Subject" ); + + result += request.headerFields() + '\n'; + result += request.headerFields( "Marc Mutz" ) + '\n'; + result += request.headerFields( "Mutz, Marc" ) + '\n'; + result += request.headerFields( "Marc Mötz" ) + '\n'; + + //cout << "Result:\n" << result.data() << endl; + + return result == expected ? 0 : 1 ; +} + +#include "request.cc" + diff --git a/kioslave/smtp/test_responseparser.cc b/kioslave/smtp/test_responseparser.cc new file mode 100644 index 000000000..5daa2fb3b --- /dev/null +++ b/kioslave/smtp/test_responseparser.cc @@ -0,0 +1,107 @@ +#include "response.h" +#include <assert.h> + +static const QCString singleLineResponseCRLF = "250 OK\r\n"; +static const QCString singleLineResponse = "250 OK"; + +static const QCString multiLineResponse[] = { + "250-ktown.kde.org\r\n", + "250-STARTTLS\r\n", + "250-AUTH PLAIN DIGEST-MD5\r\n", + "250 PIPELINING\r\n" +}; +static const unsigned int numMultiLineLines = sizeof multiLineResponse / sizeof *multiLineResponse ; + +int main ( int, char** ) { + + KioSMTP::Response r; + assert( r.isValid() ); + assert( r.lines().empty() ); + assert( r.isWellFormed() ); + assert( r.code() == 0 ); + assert( r.isUnknown() ); + assert( !r.isComplete() ); + assert( !r.isOk() ); + r.parseLine( singleLineResponseCRLF.data(), + singleLineResponseCRLF.length() ); + assert( r.isWellFormed() ); + assert( r.isComplete() ); + assert( r.isValid() ); + assert( r.isPositive() ); + assert( r.isOk() ); + assert( r.code() == 250 ); + assert( r.errorCode() == 0 ); + assert( r.first() == 2 ); + assert( r.second() == 5 ); + assert( r.third() == 0 ); + assert( r.lines().count() == 1 ); + assert( r.lines().front() == "OK" ); + r.parseLine( singleLineResponse.data(), + singleLineResponse.length() ); + assert( !r.isValid() ); + r.clear(); + assert( r.isValid() ); + assert( r.lines().empty() ); + + r.parseLine( singleLineResponse.data(), + singleLineResponse.length() ); + assert( r.isWellFormed() ); + assert( r.isComplete() ); + assert( r.isValid() ); + assert( r.isPositive() ); + assert( r.isOk() ); + assert( r.code() == 250 ); + assert( r.first() == 2 ); + assert( r.second() == 5 ); + assert( r.third() == 0 ); + assert( r.lines().count() == 1 ); + assert( r.lines().front() == "OK" ); + r.parseLine( singleLineResponse.data(), + singleLineResponse.length() ); + assert( !r.isValid() ); + r.clear(); + assert( r.isValid() ); + + for ( unsigned int i = 0 ; i < numMultiLineLines ; ++i ) { + r.parseLine( multiLineResponse[i].data(), + multiLineResponse[i].length() ); + assert( r.isWellFormed() ); + if ( i < numMultiLineLines-1 ) + assert( !r.isComplete() ); + else + assert( r.isComplete() ); + assert( r.isValid() ); + assert( r.isPositive() ); + assert( r.code() == 250 ); + assert( r.first() == 2 ); + assert( r.second() == 5 ); + assert( r.third() == 0 ); + assert( r.lines().count() == i + 1 ); + } + assert( r.lines().back() == "PIPELINING" ); + + r.clear(); + r.parseLine( "230", 3 ); + assert( r.isValid() ); + assert( r.isWellFormed() ); // even though it isn't ;-) + assert( r.code() == 230 ); + assert( r.lines().count() == 1 ); + assert( r.lines().front().isNull() ); + + r.clear(); + r.parseLine( "230\r\n", 5 ); + assert( r.isValid() ); + assert( r.isWellFormed() ); // even though it isn't ;-) + assert( r.code() == 230 ); + assert( r.lines().count() == 1 ); + assert( r.lines().front().isNull() ); + + r.clear(); + r.parseLine( " 23 ok", 6 ); + assert( !r.isValid() ); + assert( !r.isWellFormed() ); + + return 0; +} + +#include "response.cc" diff --git a/kioslave/smtp/transactionstate.cc b/kioslave/smtp/transactionstate.cc new file mode 100644 index 000000000..3bf3c7614 --- /dev/null +++ b/kioslave/smtp/transactionstate.cc @@ -0,0 +1,114 @@ +/* -*- c++ -*- + transactionstate.cc + + This file is part of kio_smtp, the KDE SMTP kioslave. + Copyright (c) 2003 Marc Mutz <mutz@kde.org> + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License, version 2, as + published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + In addition, as a special exception, the copyright holders give + permission to link the code of this program with any edition of + the Qt library by Trolltech AS, Norway (or with modified versions + of Qt that use the same license as Qt), and distribute linked + combinations including the two. You must obey the GNU General + Public License in all respects for all of the code used other than + Qt. If you modify this file, you may extend this exception to + your version of the file, but you are not obligated to do so. If + you do not wish to do so, delete this exception statement from + your version. +*/ + +#include <config.h> + +#include "transactionstate.h" + +#include <kio/global.h> +#include <klocale.h> + +#include <qstringlist.h> + +namespace KioSMTP { + + void TransactionState::setFailedFatally( int code, const QString & msg ) { + mFailed = mFailedFatally = true; + mErrorCode = code; + mErrorMessage = msg; + } + + void TransactionState::setMailFromFailed( const QString & addr, const Response & r ) { + setFailed(); + mErrorCode = KIO::ERR_NO_CONTENT; + if ( addr.isEmpty() ) + mErrorMessage = i18n("The server did not accept a blank sender address.\n" + "%1").arg( r.errorMessage() ); + else + mErrorMessage = i18n("The server did not accept the sender address \"%1\".\n" + "%2").arg( addr ).arg( r.errorMessage() ); + } + + void TransactionState::addRejectedRecipient( const RecipientRejection & r ) { + mRejectedRecipients.push_back( r ); + if ( mRcptToDenyIsFailure ) + setFailed(); + } + + void TransactionState::setDataCommandSucceeded( bool succeeded, const Response & r ) { + mDataCommandSucceeded = succeeded; + mDataResponse = r; + if ( !succeeded ) + setFailed(); + else if ( failed() ) + // can happen with pipelining: the server accepts the DATA, but + // we don't want to send the data, so force a connection + // shutdown: + setFailedFatally(); + } + + int TransactionState::errorCode() const { + if ( !failed() ) + return 0; + if ( mErrorCode ) + return mErrorCode; + if ( haveRejectedRecipients() || !dataCommandSucceeded() ) + return KIO::ERR_NO_CONTENT; + // ### what else? + return KIO::ERR_INTERNAL; + } + + QString TransactionState::errorMessage() const { + if ( !failed() ) + return QString::null; + + if ( !mErrorMessage.isEmpty() ) + return mErrorMessage; + + if ( haveRejectedRecipients() ) { + QString msg = i18n("Message sending failed since the following recipients were rejected by the server:\n" + "%1"); + QStringList recip; + for ( RejectedRecipientList::const_iterator it = mRejectedRecipients.begin() ; + it != mRejectedRecipients.end() ; ++it ) + recip.push_back( (*it).recipient + " (" + (*it).reason + ')' ); + return msg.arg( recip.join("\n") ); + } + + if ( !dataCommandSucceeded() ) + return i18n("The attempt to start sending the message content failed.\n" + "%1").arg( mDataResponse.errorMessage() ); + + // ### what else? + return i18n("Unhandled error condition. Please send a bug report."); + } + +} diff --git a/kioslave/smtp/transactionstate.h b/kioslave/smtp/transactionstate.h new file mode 100644 index 000000000..96376718e --- /dev/null +++ b/kioslave/smtp/transactionstate.h @@ -0,0 +1,185 @@ +/* -*- c++ -*- + transactionstate.h + + This file is part of kio_smtp, the KDE SMTP kioslave. + Copyright (c) 2003 Marc Mutz <mutz@kde.org> + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License, version 2, as + published by the Free Software Foundation. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + In addition, as a special exception, the copyright holders give + permission to link the code of this program with any edition of + the Qt library by Trolltech AS, Norway (or with modified versions + of Qt that use the same license as Qt), and distribute linked + combinations including the two. You must obey the GNU General + Public License in all respects for all of the code used other than + Qt. If you modify this file, you may extend this exception to + your version of the file, but you are not obligated to do so. If + you do not wish to do so, delete this exception statement from + your version. +*/ + +#ifndef __KIOSMTP_TRANSACTIONSTATE_H__ +#define __KIOSMTP_TRANSACTIONSTATE_H__ + +#include "response.h" + +#include <qstring.h> +#include <qvaluelist.h> + +namespace KioSMTP { + + /** + @short A class modelling an SMTP transaction's state + + This class models SMTP transaction state, ie. the collective + result of the MAIL FROM:, RCPT TO: and DATA commands. This is + needed since e.g. a single failed RCPT TO: command does not + neccessarily fail the whole transaction (servers are free to + accept delivery for some recipients, but not for others). + + The class can operate in two modes, which differ in the way + failed recipients are handled. If @p rcptToDenyIsFailure is true + (the default), then any failing RCPT TO: will cause the + transaction to fail. Since at the point of RCPT TO: failure + detection, the DATA command may have already been sent + (pipelining), the only way to cancel the transaction is to take + down the connection hard (ie. without proper quit). + + Since that is not very nice behaviour, a second mode that is more + to the spirit of SMTP is provided that can cope with partially + failed RCPT TO: commands. + */ + class TransactionState { + public: + struct RecipientRejection { + RecipientRejection( const QString & who=QString::null, + const QString & why=QString::null ) + : recipient( who ), reason( why ) {} + QString recipient; + QString reason; +#ifdef KIOSMTP_COMPARATORS + bool operator==( const RecipientRejection & other ) const { + return recipient == other.recipient && reason == other.reason; + } +#endif + }; + typedef QValueList<RecipientRejection> RejectedRecipientList; + + TransactionState( bool rcptToDenyIsFailure=true ) + : mErrorCode( 0 ), + mRcptToDenyIsFailure( rcptToDenyIsFailure ), + mAtLeastOneRecipientWasAccepted( false ), + mDataCommandIssued( false ), + mDataCommandSucceeded( false ), + mFailed( false ), + mFailedFatally( false ), + mComplete( false ) {} + + /** @return whether the transaction failed (e.g. the server + rejected all recipients. Graceful failure is handled after + transaction ends. */ + bool failed() const { return mFailed || mFailedFatally; } + void setFailed() { mFailed = true; } + + /** @return whether the failure was so grave that an immediate + untidy connection shutdown is in order (ie. @ref + smtp_close(false)). Fatal failure is handled immediately */ + bool failedFatally() const { return mFailedFatally; } + void setFailedFatally( int code=0, const QString & msg=QString::null ); + + /** @return whether the transaction was completed successfully */ + bool complete() const { return mComplete; } + void setComplete() { mComplete = true; } + + /** @return an appropriate KIO error code in case the transaction + failed, or 0 otherwise */ + int errorCode() const; + /** @return an appropriate error message in case the transaction + failed or QString::null otherwise */ + QString errorMessage() const; + + void setMailFromFailed( const QString & addr, const Response & r ); + + bool dataCommandIssued() const { return mDataCommandIssued; } + void setDataCommandIssued( bool issued ) { mDataCommandIssued = issued; } + + bool dataCommandSucceeded() const { + return mDataCommandIssued && mDataCommandSucceeded; + } + void setDataCommandSucceeded( bool succeeded, const Response & r ); + + Response dataResponse() const { + return mDataResponse; + } + + bool atLeastOneRecipientWasAccepted() const { + return mAtLeastOneRecipientWasAccepted; + } + void setRecipientAccepted() { + mAtLeastOneRecipientWasAccepted = true; + } + + bool haveRejectedRecipients() const { + return !mRejectedRecipients.empty(); + } + RejectedRecipientList rejectedRecipients() const { + return mRejectedRecipients; + } + void addRejectedRecipient( const RecipientRejection & r ); + void addRejectedRecipient( const QString & who, const QString & why ) { + addRejectedRecipient( RecipientRejection( who, why ) ); + } + + void clear() { + mRejectedRecipients.clear(); + mDataResponse.clear(); + mAtLeastOneRecipientWasAccepted + = mDataCommandIssued + = mDataCommandSucceeded + = mFailed = mFailedFatally + = mComplete = false; + } + +#ifdef KIOSMTP_COMPARATORS + bool operator==( const TransactionState & other ) const { + return + mAtLeastOneRecipientWasAccepted == other.mAtLeastOneRecipientWasAccepted && + mDataCommandIssued == other.mDataCommandIssued && + mDataCommandSucceeded == other.mDataCommandSucceeded && + mFailed == other.mFailed && + mFailedFatally == other.mFailedFatally && + mComplete == other.mComplete && + mDataResponse.code() == other.mDataResponse.code() && + mRejectedRecipients == other.mRejectedRecipients; + } +#endif + + + private: + RejectedRecipientList mRejectedRecipients; + Response mDataResponse; + QString mErrorMessage; + int mErrorCode; + bool mRcptToDenyIsFailure; + bool mAtLeastOneRecipientWasAccepted; + bool mDataCommandIssued; + bool mDataCommandSucceeded; + bool mFailed; + bool mFailedFatally; + bool mComplete; + }; + +} // namespace KioSMTP + +#endif // __KIOSMTP_TRANSACTIONSTATE_H__ |