// // VMime library (http://www.vmime.org) // Copyright (C) 2002-2013 Vincent Richard // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License as // published by the Free Software Foundation; either version 3 of // the License, or (at your option) any later version. // // 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. // // Linking this library statically or dynamically with other modules is making // a combined work based on this library. Thus, the terms and conditions of // the GNU General Public License cover the whole combination. // #include "vmime/config.hpp" #if VMIME_HAVE_MESSAGING_FEATURES && VMIME_HAVE_MESSAGING_PROTO_SMTP #include "vmime/net/smtp/SMTPTransport.hpp" #include "vmime/net/smtp/SMTPResponse.hpp" #include "vmime/net/smtp/SMTPCommand.hpp" #include "vmime/net/smtp/SMTPCommandSet.hpp" #include "vmime/exception.hpp" #include "vmime/platform.hpp" #include "vmime/mailboxList.hpp" #include "vmime/utility/filteredStream.hpp" #include "vmime/utility/stringUtils.hpp" #include "vmime/utility/outputStreamSocketAdapter.hpp" #include "vmime/utility/streamUtils.hpp" #include "vmime/net/defaultConnectionInfos.hpp" #if VMIME_HAVE_SASL_SUPPORT #include "vmime/security/sasl/SASLContext.hpp" #endif // VMIME_HAVE_SASL_SUPPORT #if VMIME_HAVE_TLS_SUPPORT #include "vmime/net/tls/TLSSession.hpp" #include "vmime/net/tls/TLSSecuredConnectionInfos.hpp" #endif // VMIME_HAVE_TLS_SUPPORT // Helpers for service properties #define GET_PROPERTY(type, prop) \ (getInfos().getPropertyValue (getSession(), \ dynamic_cast (getInfos()).getProperties().prop)) #define HAS_PROPERTY(prop) \ (getInfos().hasProperty(getSession(), \ dynamic_cast (getInfos()).getProperties().prop)) namespace vmime { namespace net { namespace smtp { SMTPTransport::SMTPTransport(ref sess, ref auth, const bool secured) : transport(sess, getInfosInstance(), auth), m_socket(NULL), m_authentified(false), m_extendedSMTP(false), m_timeoutHandler(NULL), m_isSMTPS(secured), m_secured(false) { } SMTPTransport::~SMTPTransport() { try { if (isConnected()) disconnect(); else if (m_socket) internalDisconnect(); } catch (vmime::exception&) { // Ignore } } const string SMTPTransport::getProtocolName() const { return "smtp"; } void SMTPTransport::connect() { if (isConnected()) throw exceptions::already_connected(); const string address = GET_PROPERTY(string, PROPERTY_SERVER_ADDRESS); const port_t port = GET_PROPERTY(port_t, PROPERTY_SERVER_PORT); // Create the time-out handler if (getTimeoutHandlerFactory()) m_timeoutHandler = getTimeoutHandlerFactory()->create(); // Create and connect the socket m_socket = getSocketFactory()->create(m_timeoutHandler); #if VMIME_HAVE_TLS_SUPPORT if (m_isSMTPS) // dedicated port/SMTPS { ref tlsSession = tls::TLSSession::create(getCertificateVerifier()); ref tlsSocket = tlsSession->getSocket(m_socket); m_socket = tlsSocket; m_secured = true; m_cntInfos = vmime::create (address, port, tlsSession, tlsSocket); } else #endif // VMIME_HAVE_TLS_SUPPORT { m_cntInfos = vmime::create (address, port); } m_socket->connect(address, port); // Connection // // eg: C: // --- S: 220 smtp.domain.com Service ready ref resp; if ((resp = readResponse())->getCode() != 220) { internalDisconnect(); throw exceptions::connection_greeting_error(resp->getText()); } // Identification helo(); #if VMIME_HAVE_TLS_SUPPORT // Setup secured connection, if requested const bool tls = HAS_PROPERTY(PROPERTY_CONNECTION_TLS) && GET_PROPERTY(bool, PROPERTY_CONNECTION_TLS); const bool tlsRequired = HAS_PROPERTY(PROPERTY_CONNECTION_TLS_REQUIRED) && GET_PROPERTY(bool, PROPERTY_CONNECTION_TLS_REQUIRED); if (!m_isSMTPS && tls) // only if not SMTPS { try { startTLS(); } // Non-fatal error catch (exceptions::command_error&) { if (tlsRequired) { throw; } else { // TLS is not required, so don't bother } } // Fatal error catch (...) { throw; } // Must reissue a EHLO command [RFC-2487, 5.2] helo(); } #endif // VMIME_HAVE_TLS_SUPPORT // Authentication if (GET_PROPERTY(bool, PROPERTY_OPTIONS_NEEDAUTH)) authenticate(); else m_authentified = true; } void SMTPTransport::helo() { // First, try Extended SMTP (ESMTP) // // eg: C: EHLO thismachine.ourdomain.com // S: 250-smtp.theserver.com // S: 250-AUTH CRAM-MD5 DIGEST-MD5 // S: 250-PIPELINING // S: 250 SIZE 2555555555 sendRequest(SMTPCommand::EHLO(platform::getHandler()->getHostName())); ref resp; if ((resp = readResponse())->getCode() != 250) { // Next, try "Basic" SMTP // // eg: C: HELO thismachine.ourdomain.com // S: 250 OK sendRequest(SMTPCommand::HELO(platform::getHandler()->getHostName())); if ((resp = readResponse())->getCode() != 250) { internalDisconnect(); throw exceptions::connection_greeting_error(resp->getLastLine().getText()); } m_extendedSMTP = false; m_extensions.clear(); } else { m_extendedSMTP = true; m_extensions.clear(); // Get supported extensions from SMTP response // One extension per line, format is: EXT PARAM1 PARAM2... for (size_t i = 1, n = resp->getLineCount() ; i < n ; ++i) { const string line = resp->getLineAt(i).getText(); std::istringstream iss(line); string ext; iss >> ext; std::vector params; string param; // Special case: some servers send "AUTH=MECH [MECH MECH...]" if (ext.length() >= 5 && utility::stringUtils::toUpper(ext.substr(0, 5)) == "AUTH=") { params.push_back(utility::stringUtils::toUpper(ext.substr(5))); ext = "AUTH"; } while (iss >> param) params.push_back(utility::stringUtils::toUpper(param)); m_extensions[ext] = params; } } } void SMTPTransport::authenticate() { if (!m_extendedSMTP) { internalDisconnect(); throw exceptions::command_error("AUTH", "ESMTP not supported."); } getAuthenticator()->setService(thisRef().dynamicCast ()); #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 e; } } #endif // VMIME_HAVE_SASL_SUPPORT // No other authentication method is possible throw exceptions::authentication_error("All authentication methods failed"); } #if VMIME_HAVE_SASL_SUPPORT void SMTPTransport::authenticateSASL() { if (!getAuthenticator().dynamicCast ()) throw exceptions::authentication_error("No SASL authenticator available."); // Obtain SASL mechanisms supported by server from ESMTP extensions const std::vector saslMechs = (m_extensions.find("AUTH") != m_extensions.end()) ? m_extensions["AUTH"] : std::vector (); if (saslMechs.empty()) throw exceptions::authentication_error("No SASL mechanism available."); std::vector > mechList; ref saslContext = vmime::create (); for (unsigned int i = 0 ; i < saslMechs.size() ; ++i) { try { mechList.push_back (saslContext->createMechanism(saslMechs[i])); } catch (exceptions::no_such_mechanism&) { // Ignore mechanism } } if (mechList.empty()) throw exceptions::authentication_error("No SASL mechanism available."); // Try to suggest a mechanism among all those supported ref suggestedMech = saslContext->suggestMechanism(mechList); if (!suggestedMech) throw exceptions::authentication_error("Unable to suggest SASL mechanism."); // Allow application to choose which mechanisms to use mechList = getAuthenticator().dynamicCast ()-> getAcceptableMechanisms(mechList, suggestedMech); if (mechList.empty()) throw exceptions::authentication_error("No SASL mechanism available."); // Try each mechanism in the list in turn for (unsigned int i = 0 ; i < mechList.size() ; ++i) { ref mech = mechList[i]; ref saslSession = saslContext->createSession("smtp", getAuthenticator(), mech); saslSession->init(); sendRequest(SMTPCommand::AUTH(mech->getName())); for (bool cont = true ; cont ; ) { ref response = readResponse(); switch (response->getCode()) { case 235: { m_socket = saslSession->getSecuredSocket(m_socket); return; } case 334: { byte_t* challenge = 0; long challengeLen = 0; byte_t* resp = 0; long respLen = 0; try { // Extract challenge saslContext->decodeB64(response->getText(), &challenge, &challengeLen); // Prepare response saslSession->evaluateChallenge (challenge, challengeLen, &resp, &respLen); // Send response m_socket->send(saslContext->encodeB64(resp, respLen) + "\r\n"); } catch (exceptions::sasl_exception& e) { if (challenge) { delete [] challenge; challenge = NULL; } if (resp) { delete [] resp; resp = NULL; } // Cancel SASL exchange m_socket->sendRaw("*\r\n", 3); } catch (...) { if (challenge) delete [] challenge; if (resp) delete [] resp; throw; } if (challenge) delete [] challenge; if (resp) delete [] resp; break; } default: cont = false; break; } } } throw exceptions::authentication_error ("Could not authenticate using SASL: all mechanisms failed."); } #endif // VMIME_HAVE_SASL_SUPPORT #if VMIME_HAVE_TLS_SUPPORT void SMTPTransport::startTLS() { try { sendRequest(SMTPCommand::STARTTLS()); ref resp = readResponse(); if (resp->getCode() != 220) throw exceptions::command_error("STARTTLS", resp->getText()); ref tlsSession = tls::TLSSession::create(getCertificateVerifier()); ref tlsSocket = tlsSession->getSocket(m_socket); tlsSocket->handshake(m_timeoutHandler); m_socket = tlsSocket; m_secured = true; m_cntInfos = vmime::create (m_cntInfos->getHost(), m_cntInfos->getPort(), tlsSession, tlsSocket); } catch (exceptions::command_error&) { // Non-fatal error throw; } catch (exception&) { // Fatal error internalDisconnect(); throw; } } #endif // VMIME_HAVE_TLS_SUPPORT bool SMTPTransport::isConnected() const { return (m_socket && m_socket->isConnected() && m_authentified); } bool SMTPTransport::isSecuredConnection() const { return m_secured; } ref SMTPTransport::getConnectionInfos() const { return m_cntInfos; } void SMTPTransport::disconnect() { if (!isConnected()) throw exceptions::not_connected(); internalDisconnect(); } void SMTPTransport::internalDisconnect() { try { sendRequest(SMTPCommand::QUIT()); readResponse(); } catch (exception&) { // Not important } m_socket->disconnect(); m_socket = NULL; m_timeoutHandler = NULL; m_authentified = false; m_extendedSMTP = false; m_secured = false; m_cntInfos = NULL; } void SMTPTransport::noop() { if (!isConnected()) throw exceptions::not_connected(); sendRequest(SMTPCommand::NOOP()); ref resp = readResponse(); if (resp->getCode() != 250) throw exceptions::command_error("NOOP", resp->getText()); } void SMTPTransport::send(const mailbox& expeditor, const mailboxList& recipients, utility::inputStream& is, const utility::stream::size_type size, utility::progressListener* progress) { if (!isConnected()) throw exceptions::not_connected(); // If no recipient/expeditor was found, throw an exception if (recipients.isEmpty()) throw exceptions::no_recipient(); else if (expeditor.isEmpty()) throw exceptions::no_expeditor(); const bool hasPipelining = m_extensions.find("PIPELINING") != m_extensions.end(); ref resp; ref commands = SMTPCommandSet::create(hasPipelining); // Emit the "MAIL" command commands->addCommand(SMTPCommand::MAIL(expeditor)); // Emit a "RCPT TO" command for each recipient for (size_t i = 0 ; i < recipients.getMailboxCount() ; ++i) { const mailbox& mbox = *recipients.getMailboxAt(i); commands->addCommand(SMTPCommand::RCPT(mbox)); } // Prepare sending of message data commands->addCommand(SMTPCommand::DATA()); // Read response for "MAIL" command commands->writeToSocket(m_socket); if ((resp = readResponse())->getCode() != 250) { internalDisconnect(); throw exceptions::command_error(commands->getLastCommandSent()->getText(), resp->getText()); } // Read responses for "RCPT TO" commands for (size_t i = 0 ; i < recipients.getMailboxCount() ; ++i) { commands->writeToSocket(m_socket); if ((resp = readResponse())->getCode() != 250) { internalDisconnect(); throw exceptions::command_error(commands->getLastCommandSent()->getText(), resp->getText()); } } // Read response for "DATA" command commands->writeToSocket(m_socket); if ((resp = readResponse())->getCode() != 354) { internalDisconnect(); throw exceptions::command_error(commands->getLastCommandSent()->getText(), resp->getText()); } // Send the message data // Stream copy with "\n." to "\n.." transformation utility::outputStreamSocketAdapter sos(*m_socket); utility::dotFilteredOutputStream fos(sos); utility::bufferedStreamCopy(is, fos, size, progress); fos.flush(); // Send end-of-data delimiter m_socket->sendRaw("\r\n.\r\n", 5); if ((resp = readResponse())->getCode() != 250) { internalDisconnect(); throw exceptions::command_error("DATA", resp->getText()); } } void SMTPTransport::sendRequest(ref cmd) { cmd->writeToSocket(m_socket); } ref SMTPTransport::readResponse() { ref resp = SMTPResponse::readResponse (m_socket, m_timeoutHandler, m_responseState); m_responseState = resp->getCurrentState(); return resp; } // Service infos SMTPServiceInfos SMTPTransport::sm_infos(false); const serviceInfos& SMTPTransport::getInfosInstance() { return sm_infos; } const serviceInfos& SMTPTransport::getInfos() const { return sm_infos; } } // smtp } // net } // vmime #endif // VMIME_HAVE_MESSAGING_FEATURES && VMIME_HAVE_MESSAGING_PROTO_SMTP