504 lines
11 KiB
C++
504 lines
11 KiB
C++
![]() |
//
|
||
|
// VMime library (http://www.vmime.org)
|
||
|
// Copyright (C) 2002-2005 Vincent Richard <vincent@vincent-richard.net>
|
||
|
//
|
||
|
// 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 2 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., 675 Mass Ave, Cambridge, MA 02139, USA.
|
||
|
//
|
||
|
|
||
|
#include "vmime/net/smtp/SMTPTransport.hpp"
|
||
|
|
||
|
#include "vmime/exception.hpp"
|
||
|
#include "vmime/platformDependant.hpp"
|
||
|
#include "vmime/encoderB64.hpp"
|
||
|
#include "vmime/mailboxList.hpp"
|
||
|
|
||
|
#include "vmime/net/authHelper.hpp"
|
||
|
|
||
|
#include "vmime/utility/filteredStream.hpp"
|
||
|
|
||
|
|
||
|
// Helpers for service properties
|
||
|
#define GET_PROPERTY(type, prop) \
|
||
|
(sm_infos.getPropertyValue <type>(getSession(), sm_infos.getProperties().prop))
|
||
|
#define HAS_PROPERTY(prop) \
|
||
|
(sm_infos.hasProperty(getSession(), sm_infos.getProperties().prop))
|
||
|
|
||
|
|
||
|
namespace vmime {
|
||
|
namespace net {
|
||
|
namespace smtp {
|
||
|
|
||
|
|
||
|
SMTPTransport::SMTPTransport(ref <session> sess, ref <authenticator> auth)
|
||
|
: transport(sess, getInfosInstance(), auth), m_socket(NULL),
|
||
|
m_authentified(false), m_extendedSMTP(false), m_timeoutHandler(NULL)
|
||
|
{
|
||
|
}
|
||
|
|
||
|
|
||
|
SMTPTransport::~SMTPTransport()
|
||
|
{
|
||
|
if (isConnected())
|
||
|
disconnect();
|
||
|
else if (m_socket)
|
||
|
internalDisconnect();
|
||
|
}
|
||
|
|
||
|
|
||
|
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 (HAS_PROPERTY(PROPERTY_TIMEOUT_FACTORY))
|
||
|
{
|
||
|
timeoutHandlerFactory* tof = platformDependant::getHandler()->
|
||
|
getTimeoutHandlerFactory(GET_PROPERTY(string, PROPERTY_TIMEOUT_FACTORY));
|
||
|
|
||
|
m_timeoutHandler = tof->create();
|
||
|
}
|
||
|
|
||
|
// Create and connect the socket
|
||
|
socketFactory* sf = platformDependant::getHandler()->
|
||
|
getSocketFactory(GET_PROPERTY(string, PROPERTY_SERVER_SOCKETFACTORY));
|
||
|
|
||
|
m_socket = sf->create();
|
||
|
m_socket->connect(address, port);
|
||
|
|
||
|
// Connection
|
||
|
//
|
||
|
// eg: C: <connection to server>
|
||
|
// --- S: 220 smtp.domain.com Service ready
|
||
|
|
||
|
string response;
|
||
|
readResponse(response);
|
||
|
|
||
|
if (responseCode(response) != 220)
|
||
|
{
|
||
|
internalDisconnect();
|
||
|
throw exceptions::connection_greeting_error(response);
|
||
|
}
|
||
|
|
||
|
// Identification
|
||
|
// First, try Extended SMTP (ESMTP)
|
||
|
//
|
||
|
// eg: C: EHLO thismachine.ourdomain.com
|
||
|
// S: 250 OK
|
||
|
|
||
|
sendRequest("EHLO " + platformDependant::getHandler()->getHostName());
|
||
|
readResponse(response);
|
||
|
|
||
|
if (responseCode(response) != 250)
|
||
|
{
|
||
|
// Next, try "Basic" SMTP
|
||
|
//
|
||
|
// eg: C: HELO thismachine.ourdomain.com
|
||
|
// S: 250 OK
|
||
|
|
||
|
sendRequest("HELO " + platformDependant::getHandler()->getHostName());
|
||
|
readResponse(response);
|
||
|
|
||
|
if (responseCode(response) != 250)
|
||
|
{
|
||
|
internalDisconnect();
|
||
|
throw exceptions::connection_greeting_error(response);
|
||
|
}
|
||
|
|
||
|
m_extendedSMTP = false;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
m_extendedSMTP = true;
|
||
|
}
|
||
|
|
||
|
// Authentication
|
||
|
if (GET_PROPERTY(bool, PROPERTY_OPTIONS_NEEDAUTH))
|
||
|
{
|
||
|
if (!m_extendedSMTP)
|
||
|
{
|
||
|
internalDisconnect();
|
||
|
throw exceptions::command_error("AUTH", "ESMTP not supported.");
|
||
|
}
|
||
|
|
||
|
const authenticationInfos auth = getAuthenticator()->requestAuthInfos();
|
||
|
bool authentified = false;
|
||
|
|
||
|
enum AuthMethods
|
||
|
{
|
||
|
First = 0,
|
||
|
CRAM_MD5 = First,
|
||
|
// TODO: more authentication methods...
|
||
|
End
|
||
|
};
|
||
|
|
||
|
for (int currentMethod = First ; !authentified ; ++currentMethod)
|
||
|
{
|
||
|
switch (currentMethod)
|
||
|
{
|
||
|
case CRAM_MD5:
|
||
|
{
|
||
|
sendRequest("AUTH CRAM-MD5");
|
||
|
readResponse(response);
|
||
|
|
||
|
if (responseCode(response) == 334)
|
||
|
{
|
||
|
encoderB64 base64;
|
||
|
|
||
|
string challengeB64 = responseText(response);
|
||
|
string challenge, challengeHex;
|
||
|
|
||
|
{
|
||
|
utility::inputStreamStringAdapter in(challengeB64);
|
||
|
utility::outputStreamStringAdapter out(challenge);
|
||
|
|
||
|
base64.decode(in, out);
|
||
|
}
|
||
|
|
||
|
hmac_md5(challenge, auth.getPassword(), challengeHex);
|
||
|
|
||
|
string decoded = auth.getUsername() + " " + challengeHex;
|
||
|
string encoded;
|
||
|
|
||
|
{
|
||
|
utility::inputStreamStringAdapter in(decoded);
|
||
|
utility::outputStreamStringAdapter out(encoded);
|
||
|
|
||
|
base64.encode(in, out);
|
||
|
}
|
||
|
|
||
|
sendRequest(encoded);
|
||
|
readResponse(response);
|
||
|
|
||
|
if (responseCode(response) == 235)
|
||
|
{
|
||
|
authentified = true;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
internalDisconnect();
|
||
|
throw exceptions::authentication_error(response);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
break;
|
||
|
}
|
||
|
case End:
|
||
|
{
|
||
|
// All authentication methods have been tried and
|
||
|
// the server does not understand any.
|
||
|
throw exceptions::authentication_error(response);
|
||
|
}
|
||
|
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
m_authentified = true;
|
||
|
}
|
||
|
|
||
|
|
||
|
const bool SMTPTransport::isConnected() const
|
||
|
{
|
||
|
return (m_socket && m_socket->isConnected() && m_authentified);
|
||
|
}
|
||
|
|
||
|
|
||
|
void SMTPTransport::disconnect()
|
||
|
{
|
||
|
if (!isConnected())
|
||
|
throw exceptions::not_connected();
|
||
|
|
||
|
internalDisconnect();
|
||
|
}
|
||
|
|
||
|
|
||
|
void SMTPTransport::internalDisconnect()
|
||
|
{
|
||
|
sendRequest("QUIT");
|
||
|
|
||
|
m_socket->disconnect();
|
||
|
m_socket = NULL;
|
||
|
|
||
|
m_timeoutHandler = NULL;
|
||
|
|
||
|
m_authentified = false;
|
||
|
m_extendedSMTP = false;
|
||
|
}
|
||
|
|
||
|
|
||
|
void SMTPTransport::noop()
|
||
|
{
|
||
|
m_socket->send("NOOP");
|
||
|
|
||
|
string response;
|
||
|
readResponse(response);
|
||
|
|
||
|
if (responseCode(response) != 250)
|
||
|
throw exceptions::command_error("NOOP", response);
|
||
|
}
|
||
|
|
||
|
|
||
|
void SMTPTransport::send(const mailbox& expeditor, const mailboxList& recipients,
|
||
|
utility::inputStream& is, const utility::stream::size_type size,
|
||
|
utility::progressionListener* progress)
|
||
|
{
|
||
|
// If no recipient/expeditor was found, throw an exception
|
||
|
if (recipients.isEmpty())
|
||
|
throw exceptions::no_recipient();
|
||
|
else if (expeditor.isEmpty())
|
||
|
throw exceptions::no_expeditor();
|
||
|
|
||
|
// Emit the "MAIL" command
|
||
|
string response;
|
||
|
|
||
|
sendRequest("MAIL FROM: <" + expeditor.getEmail() + ">");
|
||
|
readResponse(response);
|
||
|
|
||
|
if (responseCode(response) != 250)
|
||
|
{
|
||
|
internalDisconnect();
|
||
|
throw exceptions::command_error("MAIL", response);
|
||
|
}
|
||
|
|
||
|
// Emit a "RCPT TO" command for each recipient
|
||
|
for (int i = 0 ; i < recipients.getMailboxCount() ; ++i)
|
||
|
{
|
||
|
const mailbox& mbox = *recipients.getMailboxAt(i);
|
||
|
|
||
|
sendRequest("RCPT TO: <" + mbox.getEmail() + ">");
|
||
|
readResponse(response);
|
||
|
|
||
|
if (responseCode(response) != 250)
|
||
|
{
|
||
|
internalDisconnect();
|
||
|
throw exceptions::command_error("RCPT TO", response);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Send the message data
|
||
|
sendRequest("DATA");
|
||
|
readResponse(response);
|
||
|
|
||
|
if (responseCode(response) != 354)
|
||
|
{
|
||
|
internalDisconnect();
|
||
|
throw exceptions::command_error("DATA", response);
|
||
|
}
|
||
|
|
||
|
// Stream copy with "\n." to "\n.." transformation
|
||
|
utility::outputStreamSocketAdapter sos(*m_socket);
|
||
|
utility::dotFilteredOutputStream fos(sos);
|
||
|
|
||
|
utility::bufferedStreamCopy(is, fos, size, progress);
|
||
|
|
||
|
// Send end-of-data delimiter
|
||
|
m_socket->sendRaw("\r\n.\r\n", 5);
|
||
|
readResponse(response);
|
||
|
|
||
|
if (responseCode(response) != 250)
|
||
|
{
|
||
|
internalDisconnect();
|
||
|
throw exceptions::command_error("DATA", response);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
void SMTPTransport::sendRequest(const string& buffer, const bool end)
|
||
|
{
|
||
|
m_socket->send(buffer);
|
||
|
if (end) m_socket->send("\r\n");
|
||
|
}
|
||
|
|
||
|
|
||
|
const int SMTPTransport::responseCode(const string& response)
|
||
|
{
|
||
|
int code = 0;
|
||
|
|
||
|
if (response.length() >= 3)
|
||
|
{
|
||
|
code = (response[0] - '0') * 100
|
||
|
+ (response[1] - '0') * 10
|
||
|
+ (response[2] - '0');
|
||
|
}
|
||
|
|
||
|
return (code);
|
||
|
}
|
||
|
|
||
|
|
||
|
const string SMTPTransport::responseText(const string& response)
|
||
|
{
|
||
|
string text;
|
||
|
|
||
|
std::istringstream iss(response);
|
||
|
std::string line;
|
||
|
|
||
|
while (std::getline(iss, line))
|
||
|
{
|
||
|
if (line.length() >= 4)
|
||
|
text += line.substr(4);
|
||
|
else
|
||
|
text += line;
|
||
|
|
||
|
text += "\n";
|
||
|
}
|
||
|
|
||
|
return (text);
|
||
|
}
|
||
|
|
||
|
|
||
|
void SMTPTransport::readResponse(string& buffer)
|
||
|
{
|
||
|
bool foundTerminator = false;
|
||
|
|
||
|
buffer.clear();
|
||
|
|
||
|
for ( ; !foundTerminator ; )
|
||
|
{
|
||
|
// Check whether the time-out delay is elapsed
|
||
|
if (m_timeoutHandler && m_timeoutHandler->isTimeOut())
|
||
|
{
|
||
|
if (!m_timeoutHandler->handleTimeOut())
|
||
|
throw exceptions::operation_timed_out();
|
||
|
}
|
||
|
|
||
|
// Receive data from the socket
|
||
|
string receiveBuffer;
|
||
|
m_socket->receive(receiveBuffer);
|
||
|
|
||
|
if (receiveBuffer.empty()) // buffer is empty
|
||
|
{
|
||
|
platformDependant::getHandler()->wait();
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// We have received data: reset the time-out counter
|
||
|
if (m_timeoutHandler)
|
||
|
m_timeoutHandler->resetTimeOut();
|
||
|
|
||
|
// 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;
|
||
|
|
||
|
for ( ; !end ; --p)
|
||
|
{
|
||
|
if (p == 0 || buffer[p] == '\n')
|
||
|
{
|
||
|
end = true;
|
||
|
|
||
|
if (p + 4 < buffer.length())
|
||
|
foundTerminator = true;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Remove [CR]LF at the end of the response
|
||
|
if (buffer.length() >= 2 && buffer[buffer.length() - 1] == '\n')
|
||
|
{
|
||
|
if (buffer[buffer.length() - 2] == '\r')
|
||
|
buffer.resize(buffer.length() - 2);
|
||
|
else
|
||
|
buffer.resize(buffer.length() - 1);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
// Service infos
|
||
|
|
||
|
SMTPTransport::_infos SMTPTransport::sm_infos;
|
||
|
|
||
|
|
||
|
const serviceInfos& SMTPTransport::getInfosInstance()
|
||
|
{
|
||
|
return (sm_infos);
|
||
|
}
|
||
|
|
||
|
|
||
|
const serviceInfos& SMTPTransport::getInfos() const
|
||
|
{
|
||
|
return (sm_infos);
|
||
|
}
|
||
|
|
||
|
|
||
|
const string SMTPTransport::_infos::getPropertyPrefix() const
|
||
|
{
|
||
|
return "transport.smtp.";
|
||
|
}
|
||
|
|
||
|
|
||
|
const SMTPTransport::_infos::props& SMTPTransport::_infos::getProperties() const
|
||
|
{
|
||
|
static props p =
|
||
|
{
|
||
|
// SMTP-specific options
|
||
|
property("options.need-authentication", serviceInfos::property::TYPE_BOOL, "false"),
|
||
|
|
||
|
// Common properties
|
||
|
property(serviceInfos::property::AUTH_USERNAME),
|
||
|
property(serviceInfos::property::AUTH_PASSWORD),
|
||
|
|
||
|
property(serviceInfos::property::SERVER_ADDRESS, serviceInfos::property::FLAG_REQUIRED),
|
||
|
property(serviceInfos::property::SERVER_PORT, "25"),
|
||
|
property(serviceInfos::property::SERVER_SOCKETFACTORY),
|
||
|
|
||
|
property(serviceInfos::property::TIMEOUT_FACTORY)
|
||
|
};
|
||
|
|
||
|
return p;
|
||
|
}
|
||
|
|
||
|
|
||
|
const std::vector <serviceInfos::property> SMTPTransport::_infos::getAvailableProperties() const
|
||
|
{
|
||
|
std::vector <property> list;
|
||
|
const props& p = getProperties();
|
||
|
|
||
|
// SMTP-specific options
|
||
|
list.push_back(p.PROPERTY_OPTIONS_NEEDAUTH);
|
||
|
|
||
|
// Common properties
|
||
|
list.push_back(p.PROPERTY_AUTH_USERNAME);
|
||
|
list.push_back(p.PROPERTY_AUTH_PASSWORD);
|
||
|
|
||
|
list.push_back(p.PROPERTY_SERVER_ADDRESS);
|
||
|
list.push_back(p.PROPERTY_SERVER_PORT);
|
||
|
list.push_back(p.PROPERTY_SERVER_SOCKETFACTORY);
|
||
|
|
||
|
list.push_back(p.PROPERTY_TIMEOUT_FACTORY);
|
||
|
|
||
|
return (list);
|
||
|
}
|
||
|
|
||
|
|
||
|
} // smtp
|
||
|
} // net
|
||
|
} // vmime
|