/* Copyright (c) 2006 - 2007 Volker Krause Copyright (C) 2007 KovoKs Copyright (c) 2008 Thomas McGuire This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // Own #include "servertest.h" #include "socket.h" #include #include // Qt #include #include #include // KDE #include using namespace MailTransport; namespace MailTransport { class ServerTestPrivate { public: ServerTestPrivate( ServerTest *test ); ServerTest *const q; QString server; QString fakeHostname; QString testProtocol; MailTransport::Socket *normalSocket; MailTransport::Socket *secureSocket; QSet< int > connectionResults; QHash< int, QList > authenticationResults; QSet< ServerTest::Capability > capabilityResults; QHash< int, uint > customPorts; QTimer *normalSocketTimer; QTimer *secureSocketTimer; QTimer *progressTimer; QProgressBar *testProgress; bool secureSocketFinished; bool normalSocketFinished; bool tlsFinished; bool popSupportsTLS; int normalStage; int secureStage; int encryptionMode; bool normalPossible; bool securePossible; void finalResult(); void handleSMTPIMAPResponse( int type, const QString &text ); void sendInitialCapabilityQuery( MailTransport::Socket *socket ); bool handlePopConversation( MailTransport::Socket *socket, int type, int stage, const QString &response, bool *shouldStartTLS ); QList< int > parseAuthenticationList( const QStringList &authentications ); // slots void slotNormalPossible(); void slotNormalNotPossible(); void slotSslPossible(); void slotSslNotPossible(); void slotTlsDone(); void slotReadNormal( const QString &text ); void slotReadSecure( const QString &text ); void slotUpdateProgress(); }; } ServerTestPrivate::ServerTestPrivate( ServerTest *test ) : q( test ), testProgress( 0 ), secureSocketFinished( false ), normalSocketFinished( false ), tlsFinished( false ), normalPossible( true ), securePossible( true ) { } void ServerTestPrivate::finalResult() { if ( !secureSocketFinished || !normalSocketFinished || !tlsFinished ) { return; } kDebug() << "Modes:" << connectionResults; kDebug() << "Capabilities:" << capabilityResults; kDebug() << "Normal:" << q->normalProtocols(); kDebug() << "SSL:" << q->secureProtocols(); kDebug() << "TLS:" << q->tlsProtocols(); if ( testProgress ) { testProgress->hide(); } progressTimer->stop(); secureSocketFinished = false; normalSocketFinished = false; tlsFinished = false ; emit q->finished( connectionResults.toList() ); } QList< int > ServerTestPrivate::parseAuthenticationList( const QStringList &authentications ) { QList< int > result; for ( QStringList::ConstIterator it = authentications.begin(); it != authentications.end(); ++it ) { QString current = ( *it ).toUpper(); if ( current == QLatin1String( "LOGIN" ) ) { result << Transport::EnumAuthenticationType::LOGIN; } else if ( current == QLatin1String( "PLAIN" ) ) { result << Transport::EnumAuthenticationType::PLAIN; } else if ( current == QLatin1String( "CRAM-MD5" ) ) { result << Transport::EnumAuthenticationType::CRAM_MD5; } else if ( current == QLatin1String( "DIGEST-MD5" ) ) { result << Transport::EnumAuthenticationType::DIGEST_MD5; } else if ( current == QLatin1String( "NTLM" ) ) { result << Transport::EnumAuthenticationType::NTLM; } else if ( current == QLatin1String( "GSSAPI" ) ) { result << Transport::EnumAuthenticationType::GSSAPI; } else if ( current == QLatin1String( "ANONYMOUS" ) ) { result << Transport::EnumAuthenticationType::ANONYMOUS; } // APOP is handled by handlePopConversation() } kDebug() << authentications << result; // LOGIN doesn't offer anything over PLAIN, requires more server // roundtrips and is not an official SASL mechanism, but a MS-ism, // so only enable it if PLAIN isn't available: if ( result.contains( Transport::EnumAuthenticationType::PLAIN ) ) { result.removeAll( Transport::EnumAuthenticationType::LOGIN ); } return result; } void ServerTestPrivate::handleSMTPIMAPResponse( int type, const QString &text ) { if ( !text.contains( QLatin1String( "AUTH" ), Qt::CaseInsensitive ) ) { kDebug() << "No authentication possible"; return; } QStringList protocols; protocols << QLatin1String( "LOGIN" ) << QLatin1String( "PLAIN" ) << QLatin1String( "CRAM-MD5" ) << QLatin1String( "DIGEST-MD5" ) << QLatin1String( "NTLM" ) << QLatin1String( "GSSAPI" ) << QLatin1String( "ANONYMOUS" ); QStringList results; for ( int i = 0; i < protocols.count(); ++i ) { if ( text.contains( protocols.at( i ), Qt::CaseInsensitive ) ) { results.append( protocols.at( i ) ); } } authenticationResults[type] = parseAuthenticationList( results ); // if we couldn't parse any authentication modes, default to clear-text if ( authenticationResults[type].size() == 0 ) { authenticationResults[type] << Transport::EnumAuthenticationType::CLEAR; } kDebug() << "For type" << type << ", we have:" << authenticationResults[type]; } void ServerTestPrivate::slotNormalPossible() { normalSocketTimer->stop(); connectionResults << Transport::EnumEncryption::None; } void ServerTestPrivate::sendInitialCapabilityQuery( MailTransport::Socket *socket ) { if ( testProtocol == IMAP_PROTOCOL ) { socket->write( QLatin1String( "1 CAPABILITY" ) ); } else if ( testProtocol == SMTP_PROTOCOL ) { // Detect the hostname which we send with the EHLO command. // If there is a fake one set, use that, otherwise use the // local host name (and make sure it contains a domain, so the // server thinks it is valid). QString hostname; if ( !fakeHostname.isNull() ) { hostname = fakeHostname; } else { hostname = QHostInfo::localHostName(); if ( hostname.isEmpty() ) { hostname = QLatin1String( "localhost.invalid" ); } else if ( !hostname.contains( QChar::fromLatin1( '.' ) ) ) { hostname += QLatin1String( ".localnet" ); } } kDebug() << "Hostname for EHLO is" << hostname; socket->write( QLatin1String( "EHLO " ) + hostname ); } } void ServerTestPrivate::slotTlsDone() { // The server will not send a response after starting TLS. Therefore, we have to manually // call slotReadNormal(), because this is not triggered by a data received signal this time. slotReadNormal( QString() ); } bool ServerTestPrivate::handlePopConversation( MailTransport::Socket *socket, int type, int stage, const QString &response, bool *shouldStartTLS ) { Q_ASSERT( shouldStartTLS != 0 ); // Initial Greeting if ( stage == 0 ) { //Regexp taken from POP3 ioslave QString responseWithoutCRLF = response; responseWithoutCRLF.chop( 2 ); QRegExp re( QLatin1String( "<[A-Za-z0-9\\.\\-_]+@[A-Za-z0-9\\.\\-_]+>$" ), Qt::CaseInsensitive ); if ( responseWithoutCRLF.indexOf( re ) != -1 ) { authenticationResults[type] << Transport::EnumAuthenticationType::APOP; } //Each server is supposed to support clear text login authenticationResults[type] << Transport::EnumAuthenticationType::CLEAR; // If we are in TLS stage, the server does not send the initial greeting. // Assume that the APOP availability is the same as with an unsecured connection. if ( type == Transport::EnumEncryption::TLS && authenticationResults[Transport::EnumEncryption::None]. contains( Transport::EnumAuthenticationType::APOP ) ) { authenticationResults[Transport::EnumEncryption::TLS] << Transport::EnumAuthenticationType::APOP; } socket->write( QLatin1String( "CAPA" ) ); return true; } // CAPA result else if ( stage == 1 ) { // Example: // CAPA // +OK // TOP // USER // SASL LOGIN CRAM-MD5 // UIDL // RESP-CODES // . if ( response.contains( QLatin1String( "TOP" ) ) ) { capabilityResults += ServerTest::Top; } if ( response.contains( QLatin1String( "PIPELINING" ) ) ) { capabilityResults += ServerTest::Pipelining; } if ( response.contains( QLatin1String( "UIDL" ) ) ) { capabilityResults += ServerTest::UIDL; } if ( response.contains( QLatin1String( "STLS" ) ) ) { connectionResults << Transport::EnumEncryption::TLS; popSupportsTLS = true; } socket->write( QLatin1String( "AUTH" ) ); return true; } // AUTH response else if ( stage == 2 ) { // Example: // C: AUTH // S: +OK List of supported authentication methods follows // S: LOGIN // S: CRAM-MD5 // S:. QString formattedReply = response; // Get rid of trailling ".CRLF" formattedReply.chop( 3 ); // Get rid of the first +OK line formattedReply = formattedReply.right( formattedReply.size() - formattedReply.indexOf( QLatin1Char( '\n' ) ) - 1 ); formattedReply = formattedReply.replace( QLatin1Char( ' ' ), QLatin1Char( '-' ) ). replace( QLatin1String( "\r\n" ), QLatin1String( " " ) ); authenticationResults[type] += parseAuthenticationList( formattedReply.split( QLatin1Char( ' ' ) ) ); } *shouldStartTLS = popSupportsTLS; return false; } // slotReadNormal() handles normal (no) encryption and TLS encryption. // At first, the communication is not encrypted, but if the server supports // the STARTTLS/STLS keyword, the same authentication query is done again // with TLS. void ServerTestPrivate::slotReadNormal( const QString &text ) { Q_ASSERT( encryptionMode != Transport::EnumEncryption::SSL ); static const int tlsHandshakeStage = 42; kDebug() << "Stage" << normalStage + 1 << ", Mode" << encryptionMode; // If we are in stage 42, we just do the handshake for TLS encryption and // then reset the stage to -1, so that all authentication modes and // capabilities are queried again for TLS encryption (some servers have // different authentication methods in normal and in TLS mode). if ( normalStage == tlsHandshakeStage ) { Q_ASSERT( encryptionMode == Transport::EnumEncryption::TLS ); normalStage = -1; normalSocket->startTLS(); return; } bool shouldStartTLS = false; normalStage++; // Handle the whole POP converstation separatly, it is very different from // IMAP and SMTP if ( testProtocol == POP_PROTOCOL ) { if ( handlePopConversation( normalSocket, encryptionMode, normalStage, text, &shouldStartTLS ) ) { return; } } else { // Handle the SMTP/IMAP conversation here. We just send the EHLO command in // sendInitialCapabilityQuery. if ( normalStage == 0 ) { sendInitialCapabilityQuery( normalSocket ); return; } if ( text.contains( QLatin1String( "STARTTLS" ), Qt::CaseInsensitive ) ) { connectionResults << Transport::EnumEncryption::TLS; shouldStartTLS = true; } handleSMTPIMAPResponse( encryptionMode, text ); } // If we reach here, the normal authentication/capabilities query is completed. // Now do the same for TLS. normalSocketFinished = true; // If the server announced that STARTTLS/STLS is available, we'll add TLS to the // connection result, do the command and set the stage to 42 to start the handshake. if ( shouldStartTLS && encryptionMode == Transport::EnumEncryption::None ) { kDebug() << "Trying TLS..."; connectionResults << Transport::EnumEncryption::TLS; if ( testProtocol == POP_PROTOCOL ) { normalSocket->write( QLatin1String( "STLS" ) ); } else if ( testProtocol == IMAP_PROTOCOL ) { normalSocket->write( QLatin1String( "2 STARTTLS" ) ); } else { normalSocket->write( QLatin1String( "STARTTLS" ) ); } encryptionMode = Transport::EnumEncryption::TLS; normalStage = tlsHandshakeStage; return; } // If we reach here, either the TLS authentication/capabilities query is finished // or the server does not support the STARTTLS/STLS command. tlsFinished = true; finalResult(); } void ServerTestPrivate::slotReadSecure( const QString &text ) { secureStage++; if ( testProtocol == POP_PROTOCOL ) { bool dummy; if ( handlePopConversation( secureSocket, Transport::EnumEncryption::SSL, secureStage, text, &dummy ) ) { return; } } else { if ( secureStage == 0 ) { sendInitialCapabilityQuery( secureSocket ); return; } handleSMTPIMAPResponse( Transport::EnumEncryption::SSL, text ); } secureSocketFinished = true; finalResult(); } void ServerTestPrivate::slotNormalNotPossible() { normalSocketTimer->stop(); normalPossible = false; normalSocketFinished = true; tlsFinished = true; finalResult(); } void ServerTestPrivate::slotSslPossible() { secureSocketTimer->stop(); connectionResults << Transport::EnumEncryption::SSL; } void ServerTestPrivate::slotSslNotPossible() { secureSocketTimer->stop(); securePossible = false; secureSocketFinished = true; finalResult(); } void ServerTestPrivate::slotUpdateProgress() { if ( testProgress ) { testProgress->setValue( testProgress->value() + 1 ); } } //---------------------- end private class -----------------------// ServerTest::ServerTest( QWidget *parent ) : QWidget( parent ), d( new ServerTestPrivate( this ) ) { d->normalSocketTimer = new QTimer( this ); d->normalSocketTimer->setSingleShot( true ); connect( d->normalSocketTimer, SIGNAL(timeout()), SLOT(slotNormalNotPossible()) ); d->secureSocketTimer = new QTimer( this ); d->secureSocketTimer->setSingleShot( true ); connect( d->secureSocketTimer, SIGNAL(timeout()), SLOT(slotSslNotPossible()) ); d->progressTimer = new QTimer( this ); connect( d->progressTimer, SIGNAL(timeout()), SLOT(slotUpdateProgress()) ); } ServerTest::~ServerTest() { delete d; } void ServerTest::start() { kDebug() << d; d->connectionResults.clear(); d->authenticationResults.clear(); d->capabilityResults.clear(); d->popSupportsTLS = false; d->normalStage = -1; d->secureStage = -1; d->encryptionMode = Transport::EnumEncryption::None; d->normalPossible = true; d->securePossible = true; if ( d->testProgress ) { d->testProgress->setMaximum( 20 ); d->testProgress->setValue( 0 ); d->testProgress->setTextVisible( true ); d->testProgress->show(); d->progressTimer->start( 1000 ); } d->normalSocket = new MailTransport::Socket( this ); d->secureSocket = new MailTransport::Socket( this ); d->normalSocket->setObjectName( QLatin1String( "normal" ) ); d->normalSocket->setServer( d->server ); d->normalSocket->setProtocol( d->testProtocol ); if ( d->testProtocol == IMAP_PROTOCOL ) { d->normalSocket->setPort( IMAP_PORT ); d->secureSocket->setPort( IMAPS_PORT ); } else if ( d->testProtocol == SMTP_PROTOCOL ) { d->normalSocket->setPort( SMTP_PORT ); d->secureSocket->setPort( SMTPS_PORT ); } else if ( d->testProtocol == POP_PROTOCOL ) { d->normalSocket->setPort( POP_PORT ); d->secureSocket->setPort( POPS_PORT ); } if ( d->customPorts.contains( Transport::EnumEncryption::None ) ) { d->normalSocket->setPort( d->customPorts.value( Transport::EnumEncryption::None ) ); } if ( d->customPorts.contains( Transport::EnumEncryption::SSL ) ) { d->secureSocket->setPort( d->customPorts.value( Transport::EnumEncryption::SSL ) ); } connect( d->normalSocket, SIGNAL(connected()), SLOT(slotNormalPossible()) ); connect( d->normalSocket, SIGNAL(failed()), SLOT(slotNormalNotPossible()) ); connect( d->normalSocket, SIGNAL(data(QString)), SLOT(slotReadNormal(QString)) ); connect( d->normalSocket, SIGNAL(tlsDone()), SLOT(slotTlsDone())); d->normalSocket->reconnect(); d->normalSocketTimer->start( 10000 ); d->secureSocket->setObjectName( QLatin1String( "secure" ) ); d->secureSocket->setServer( d->server ); d->secureSocket->setProtocol( d->testProtocol + QLatin1Char( 's' ) ); d->secureSocket->setSecure( true ); connect( d->secureSocket, SIGNAL(connected()), SLOT(slotSslPossible()) ); connect( d->secureSocket, SIGNAL(failed()), SLOT(slotSslNotPossible()) ); connect( d->secureSocket, SIGNAL(data(QString)), SLOT(slotReadSecure(QString)) ); d->secureSocket->reconnect(); d->secureSocketTimer->start( 10000 ); } void ServerTest::setFakeHostname( const QString &fakeHostname ) { d->fakeHostname = fakeHostname; } QString ServerTest::fakeHostname() { return d->fakeHostname; } void ServerTest::setServer( const QString &server ) { d->server = server; } void ServerTest::setPort( Transport::EnumEncryption::type encryptionMode, uint port ) { Q_ASSERT( encryptionMode == Transport::EnumEncryption::None || encryptionMode == Transport::EnumEncryption::SSL ); d->customPorts.insert( encryptionMode, port ); } void ServerTest::setProgressBar( QProgressBar *pb ) { d->testProgress = pb; } void ServerTest::setProtocol( const QString &protocol ) { d->testProtocol = protocol; } QString ServerTest::protocol() { return d->testProtocol; } QString ServerTest::server() { return d->server; } int ServerTest::port( Transport::EnumEncryption::type encryptionMode ) { Q_ASSERT( encryptionMode == Transport::EnumEncryption::None || encryptionMode == Transport::EnumEncryption::SSL ); if ( d->customPorts.contains( encryptionMode ) ) { return d->customPorts.value( static_cast( encryptionMode ) ); } else { return -1; } } QProgressBar *ServerTest::progressBar() { return d->testProgress; } QList< int > ServerTest::normalProtocols() { return d->authenticationResults[TransportBase::EnumEncryption::None]; } bool ServerTest::isNormalPossible() { return d->normalPossible; } QList< int > ServerTest::tlsProtocols() { return d->authenticationResults[TransportBase::EnumEncryption::TLS]; } QList< int > ServerTest::secureProtocols() { return d->authenticationResults[Transport::EnumEncryption::SSL]; } bool ServerTest::isSecurePossible() { return d->securePossible; } QList< ServerTest::Capability > ServerTest::capabilities() const { return d->capabilityResults.toList(); } #include "moc_servertest.cpp"