diff options
Diffstat (limited to 'src/net/smtp/SMTPTransport.cpp')
-rw-r--r-- | src/net/smtp/SMTPTransport.cpp | 406 |
1 files changed, 285 insertions, 121 deletions
diff --git a/src/net/smtp/SMTPTransport.cpp b/src/net/smtp/SMTPTransport.cpp index 1cbffcf5..a5f034a6 100644 --- a/src/net/smtp/SMTPTransport.cpp +++ b/src/net/smtp/SMTPTransport.cpp @@ -27,6 +27,11 @@ #include "vmime/net/authHelper.hpp" #include "vmime/utility/filteredStream.hpp" +#include "vmime/utility/stringUtils.hpp" + +#if VMIME_HAVE_SASL_SUPPORT + #include "vmime/security/sasl/SASLContext.hpp" +#endif // VMIME_HAVE_SASL_SUPPORT // Helpers for service properties @@ -41,7 +46,7 @@ namespace net { namespace smtp { -SMTPTransport::SMTPTransport(ref <session> sess, ref <authenticator> auth) +SMTPTransport::SMTPTransport(ref <session> sess, ref <security::authenticator> auth) : transport(sess, getInfosInstance(), auth), m_socket(NULL), m_authentified(false), m_extendedSMTP(false), m_timeoutHandler(NULL) { @@ -50,10 +55,17 @@ SMTPTransport::SMTPTransport(ref <session> sess, ref <authenticator> auth) SMTPTransport::~SMTPTransport() { - if (isConnected()) - disconnect(); - else if (m_socket) - internalDisconnect(); + try + { + if (isConnected()) + disconnect(); + else if (m_socket) + internalDisconnect(); + } + catch (vmime::exception&) + { + // Ignore + } } @@ -87,15 +99,16 @@ void SMTPTransport::connect() m_socket = sf->create(); m_socket->connect(address, port); + m_responseBuffer.clear(); + // Connection // // eg: C: <connection to server> // --- S: 220 smtp.domain.com Service ready string response; - readResponse(response); - if (responseCode(response) != 220) + if (readAllResponses(response) != 220) { internalDisconnect(); throw exceptions::connection_greeting_error(response); @@ -105,12 +118,12 @@ void SMTPTransport::connect() // First, try Extended SMTP (ESMTP) // // eg: C: EHLO thismachine.ourdomain.com - // S: 250 OK + // S: 250-smtp.theserver.com + // S: 250 AUTH CRAM-MD5 DIGEST-MD5 sendRequest("EHLO " + platformDependant::getHandler()->getHostName()); - readResponse(response); - if (responseCode(response) != 250) + if (readAllResponses(response, true) != 250) { // Next, try "Basic" SMTP // @@ -118,9 +131,8 @@ void SMTPTransport::connect() // S: 250 OK sendRequest("HELO " + platformDependant::getHandler()->getHostName()); - readResponse(response); - if (responseCode(response) != 250) + if (readAllResponses(response) != 250) { internalDisconnect(); throw exceptions::connection_greeting_error(response); @@ -131,93 +143,231 @@ void SMTPTransport::connect() else { m_extendedSMTP = true; + m_extendedSMTPResponse = response; } // Authentication if (GET_PROPERTY(bool, PROPERTY_OPTIONS_NEEDAUTH)) + authenticate(); +} + + +void SMTPTransport::authenticate() +{ + if (!m_extendedSMTP) { - if (!m_extendedSMTP) + internalDisconnect(); + throw exceptions::command_error("AUTH", "ESMTP not supported."); + } + + getAuthenticator()->setService(thisRef().dynamicCast <service>()); + +#if VMIME_HAVE_SASL_SUPPORT + // First, try SASL authentication + if (GET_PROPERTY(bool, PROPERTY_OPTIONS_SASL)) + { + try + { + authenticateSASL(); + + m_authentified = true; + return; + } + catch (exceptions::authentication_error& e) + { + if (!GET_PROPERTY(bool, PROPERTY_OPTIONS_SASL_FALLBACK)) + { + // Can't fallback on normal authentication + internalDisconnect(); + throw e; + } + else + { + // Ignore, will try normal authentication + } + } + catch (exception& e) { internalDisconnect(); - throw exceptions::command_error("AUTH", "ESMTP not supported."); + throw e; } + } +#endif // VMIME_HAVE_SASL_SUPPORT - const authenticationInfos auth = getAuthenticator()->requestAuthInfos(); - bool authentified = false; + // No other authentication method is possible + throw exceptions::authentication_error("All authentication methods failed"); +} - enum AuthMethods - { - First = 0, - CRAM_MD5 = First, - // TODO: more authentication methods... - End - }; - for (int currentMethod = First ; !authentified ; ++currentMethod) +#if VMIME_HAVE_SASL_SUPPORT + +void SMTPTransport::authenticateSASL() +{ + if (!getAuthenticator().dynamicCast <security::sasl::SASLAuthenticator>()) + throw exceptions::authentication_error("No SASL authenticator available."); + + // Obtain SASL mechanisms supported by server from EHLO response + std::vector <string> saslMechs; + std::istringstream iss(m_extendedSMTPResponse); + + while (!iss.eof()) + { + string line; + std::getline(iss, line); + + std::istringstream liss(line); + string word; + + bool inAuth = false; + + while (liss >> word) { - switch (currentMethod) + if (word.length() == 4 && + (word[0] == 'A' || word[0] == 'a') || + (word[0] == 'U' || word[0] == 'u') || + (word[0] == 'T' || word[0] == 't') || + (word[0] == 'H' || word[0] == 'h')) { - case CRAM_MD5: + inAuth = true; + } + else if (inAuth) { - sendRequest("AUTH CRAM-MD5"); - readResponse(response); + saslMechs.push_back(word); + } + } + } - if (responseCode(response) == 334) - { - encoderB64 base64; + if (saslMechs.empty()) + throw exceptions::authentication_error("No SASL mechanism available."); - string challengeB64 = responseText(response); - string challenge, challengeHex; + std::vector <ref <security::sasl::SASLMechanism> > mechList; - { - utility::inputStreamStringAdapter in(challengeB64); - utility::outputStreamStringAdapter out(challenge); + ref <security::sasl::SASLContext> saslContext = + vmime::create <security::sasl::SASLContext>(); - base64.decode(in, out); - } + for (unsigned int i = 0 ; i < saslMechs.size() ; ++i) + { + try + { + mechList.push_back + (saslContext->createMechanism(saslMechs[i])); + } + catch (exceptions::no_such_mechanism&) + { + // Ignore mechanism + } + } - hmac_md5(challenge, auth.getPassword(), challengeHex); + if (mechList.empty()) + throw exceptions::authentication_error("No SASL mechanism available."); - string decoded = auth.getUsername() + " " + challengeHex; - string encoded; + // Try to suggest a mechanism among all those supported + ref <security::sasl::SASLMechanism> suggestedMech = + saslContext->suggestMechanism(mechList); - { - utility::inputStreamStringAdapter in(decoded); - utility::outputStreamStringAdapter out(encoded); + if (!suggestedMech) + throw exceptions::authentication_error("Unable to suggest SASL mechanism."); - base64.encode(in, out); - } + // Allow application to choose which mechanisms to use + mechList = getAuthenticator().dynamicCast <security::sasl::SASLAuthenticator>()-> + getAcceptableMechanisms(mechList, suggestedMech); - sendRequest(encoded); - readResponse(response); + if (mechList.empty()) + throw exceptions::authentication_error("No SASL mechanism available."); - if (responseCode(response) == 235) + // Try each mechanism in the list in turn + for (unsigned int i = 0 ; i < mechList.size() ; ++i) + { + ref <security::sasl::SASLMechanism> mech = mechList[i]; + + ref <security::sasl::SASLSession> saslSession = + saslContext->createSession("smtp", getAuthenticator(), mech); + + saslSession->init(); + + sendRequest("AUTH " + mech->getName()); + + for (bool cont = true ; cont ; ) + { + string response; + + switch (readAllResponses(response)) + { + case 235: + { + m_socket = saslSession->getSecuredSocket(m_socket); + return; + } + case 334: + { + byte* challenge = 0; + int challengeLen = 0; + + byte* resp = 0; + int respLen = 0; + + try + { + // Extract challenge + saslContext->decodeB64(response, &challenge, &challengeLen); + + // Prepare response + saslSession->evaluateChallenge + (challenge, challengeLen, &resp, &respLen); + + // Send response + sendRequest(saslContext->encodeB64(resp, respLen)); + } + catch (exceptions::sasl_exception& e) + { + if (challenge) { - authentified = true; + delete [] challenge; + challenge = NULL; } - else + + if (resp) { - internalDisconnect(); - throw exceptions::authentication_error(response); + delete [] resp; + resp = NULL; } + + // Cancel SASL exchange + sendRequest("*"); + } + catch (...) + { + if (challenge) + delete [] challenge; + + if (resp) + delete [] resp; + + throw; } + if (challenge) + delete [] challenge; + + if (resp) + delete [] resp; + break; } - case End: - { - // All authentication methods have been tried and - // the server does not understand any. - throw exceptions::authentication_error(response); - } + default: + cont = false; + break; } } } - m_authentified = true; + throw exceptions::authentication_error + ("Could not authenticate using SASL: all mechanisms failed."); } +#endif // VMIME_HAVE_SASL_SUPPORT + const bool SMTPTransport::isConnected() const { @@ -250,12 +400,11 @@ void SMTPTransport::internalDisconnect() void SMTPTransport::noop() { - m_socket->send("NOOP"); + sendRequest("NOOP"); string response; - readResponse(response); - if (responseCode(response) != 250) + if (readAllResponses(response) != 250) throw exceptions::command_error("NOOP", response); } @@ -274,9 +423,8 @@ void SMTPTransport::send(const mailbox& expeditor, const mailboxList& recipients string response; sendRequest("MAIL FROM: <" + expeditor.getEmail() + ">"); - readResponse(response); - if (responseCode(response) != 250) + if (readAllResponses(response) != 250) { internalDisconnect(); throw exceptions::command_error("MAIL", response); @@ -288,9 +436,8 @@ void SMTPTransport::send(const mailbox& expeditor, const mailboxList& recipients const mailbox& mbox = *recipients.getMailboxAt(i); sendRequest("RCPT TO: <" + mbox.getEmail() + ">"); - readResponse(response); - if (responseCode(response) != 250) + if (readAllResponses(response) != 250) { internalDisconnect(); throw exceptions::command_error("RCPT TO", response); @@ -299,9 +446,8 @@ void SMTPTransport::send(const mailbox& expeditor, const mailboxList& recipients // Send the message data sendRequest("DATA"); - readResponse(response); - if (responseCode(response) != 354) + if (readAllResponses(response) != 354) { internalDisconnect(); throw exceptions::command_error("DATA", response); @@ -315,9 +461,8 @@ void SMTPTransport::send(const mailbox& expeditor, const mailboxList& recipients // Send end-of-data delimiter m_socket->sendRaw("\r\n.\r\n", 5); - readResponse(response); - if (responseCode(response) != 250) + if (readAllResponses(response) != 250) { internalDisconnect(); throw exceptions::command_error("DATA", response); @@ -332,7 +477,7 @@ void SMTPTransport::sendRequest(const string& buffer, const bool end) } -const int SMTPTransport::responseCode(const string& response) +const int SMTPTransport::getResponseCode(const string& response) { int code = 0; @@ -347,35 +492,25 @@ const int SMTPTransport::responseCode(const string& response) } -const string SMTPTransport::responseText(const string& response) +const string SMTPTransport::readResponseLine() { - string text; + string currentBuffer = m_responseBuffer; - std::istringstream iss(response); - std::string line; - - while (std::getline(iss, line)) + while (true) { - if (line.length() >= 4) - text += line.substr(4); - else - text += line; - - text += "\n"; - } - - return (text); -} + // Get a line from the response buffer + string::size_type lineEnd = currentBuffer.find_first_of('\n'); + if (lineEnd != string::npos) + { + const string line(currentBuffer.begin(), currentBuffer.begin() + lineEnd); -void SMTPTransport::readResponse(string& buffer) -{ - bool foundTerminator = false; + currentBuffer.erase(currentBuffer.begin(), currentBuffer.begin() + lineEnd + 1); + m_responseBuffer = currentBuffer; - buffer.clear(); + return line; + } - for ( ; !foundTerminator ; ) - { // Check whether the time-out delay is elapsed if (m_timeoutHandler && m_timeoutHandler->isTimeOut()) { @@ -393,40 +528,61 @@ void SMTPTransport::readResponse(string& buffer) continue; } - // We have received data: reset the time-out counter - if (m_timeoutHandler) - m_timeoutHandler->resetTimeOut(); + currentBuffer += receiveBuffer; + } +} - // Append the data to the response buffer - buffer += receiveBuffer; - // Check for terminator string (and strip it if present) - if (buffer.length() >= 2 && buffer[buffer.length() - 1] == '\n') - { - string::size_type p = buffer.length() - 2; - bool end = false; +const int SMTPTransport::readResponse(string& text) +{ + string line = readResponseLine(); - for ( ; !end ; --p) - { - if (p == 0 || buffer[p] == '\n') - { - end = true; + // Special case where CRLF occurs after response code + if (line.length() < 4) + line = line + '\n' + readResponseLine(); - if (p + 4 < buffer.length()) - foundTerminator = true; - } - } - } - } + const int code = getResponseCode(line); + + m_responseContinues = (line.length() >= 4 && line[3] == '-'); + + if (line.length() > 4) + text = utility::stringUtils::trim(line.substr(4)); + else + text = utility::stringUtils::trim(line); - // Remove [CR]LF at the end of the response - if (buffer.length() >= 2 && buffer[buffer.length() - 1] == '\n') + return code; +} + + +const int SMTPTransport::readAllResponses(string& outText, const bool allText) +{ + string text; + + const int firstCode = readResponse(outText); + + if (allText) + text = outText; + + while (m_responseContinues) { - if (buffer[buffer.length() - 2] == '\r') - buffer.resize(buffer.length() - 2); - else - buffer.resize(buffer.length() - 1); + const int code = readResponse(outText); + + if (allText) + text += '\n' + outText; + + if (code != firstCode) + { + if (allText) + outText = text; + + return 0; + } } + + if (allText) + outText = text; + + return firstCode; } @@ -460,6 +616,10 @@ const SMTPTransport::_infos::props& SMTPTransport::_infos::getProperties() const { // SMTP-specific options property("options.need-authentication", serviceInfos::property::TYPE_BOOL, "false"), +#if VMIME_HAVE_SASL_SUPPORT + property("options.sasl", serviceInfos::property::TYPE_BOOL, "true"), + property("options.sasl.fallback", serviceInfos::property::TYPE_BOOL, "false"), +#endif // VMIME_HAVE_SASL_SUPPORT // Common properties property(serviceInfos::property::AUTH_USERNAME), @@ -483,6 +643,10 @@ const std::vector <serviceInfos::property> SMTPTransport::_infos::getAvailablePr // SMTP-specific options list.push_back(p.PROPERTY_OPTIONS_NEEDAUTH); +#if VMIME_HAVE_SASL_SUPPORT + list.push_back(p.PROPERTY_OPTIONS_SASL); + list.push_back(p.PROPERTY_OPTIONS_SASL_FALLBACK); +#endif // VMIME_HAVE_SASL_SUPPORT // Common properties list.push_back(p.PROPERTY_AUTH_USERNAME); |