SMTP is an essential part of email transferring and for communication, we always need servers and clients that collaborate and negotiate for sending emails.
SMTP Server is an application used to send an email and react to response codes from receiving servers and the server will have an IP address or hostname for negotiation.
An SMTP Server must speak this protocol with the client. Modern SMTP Servers must also consider methods for authenticating. Authenticating email is one of the best ways to signal to the receiving servers that the email you are sending is legitimate.
An SMTP Client allows sending of email notifications using an SMTP Server. To send emails, the SMTP Client needs to connect to an SMTP Server that will send the messages to the targeted recipients. Nowadays, SMTP Servers usually require client authentication using credentials.
The SMTP Client can send these credentials to get access to the server. Depending on the version of the SMTP Server, there might be other authentication methods LOGIN, PLAIN and CRAM-MD5.
It is core logic that must be implemented into SMTP Client is the negotiation.
A typical example of negotiation is:
The negotiation can be divided into three stages. The first one is establishing a connection at this stage we connect to SMTP Server and receive "READY" from the server. It is a signal for us to send "EHLO" or "HELLO" (It depends on the server version) by an insecure channel to get what services the server supports.
Usually, the server sends the command “STARTTLS” to upgrade from an insecure connection to a secure one using TLS or SSL. Transferring to a secure channel is the second stage, but this stage may not exist if your SMTP Server is located on a private server where you need for example a VPN from an organization that is allowed a reaching the server or any exceptional situation. The third stage is already actually determining all attributes of an email such as a sender, receivers, CC receivers, BCC receivers, a title, and a message
If you look at the “Client and Server negotiation” picture you see that the server returns a number before the response message. These numbers it is a state of response from the server like fail or success. Each of our requests has a specific response number from the SMTP server to verify whether it succeeded or failed and it allows us to determine what to do depending on the answers.
We'll assign specifics name to those numbers to be readable.
/*
* \brief response codes from host
*/
enum RessultCode {
Okay = 250,
Data = 354,
Ready = 220,
Goodbye = 221,
Accepted = 334,
AUTH_successful = 235
};
I will use the boost library for realization the SMTP Client.
Firstly, we need to determine fields for SMTP Client:
// header file
class SMTPClient {
public:
typedef boost::asio::ssl::stream<boost::asio::ip::tcp::socket> SslSocket;
...
private:
std::string serverId_;
std::stringstream message_;
boost::asio::io_service service_;
boost::asio::ssl::context sslContext_;
SslSocket sslSocket_;
boost::asio::ip::tcp::socket& socket_;
bool tlsEnabled_;
};
...
// source file
SMTPClient::SMTPClient() :
...
, sslContext_(service_, asio::ssl::context::sslv3)
, sslSocket_(service, sslContext_)
, socket_(sslSocket_.next_layer()) {
}
We need two sockets for secure and insecure connection. In newer versions of the boost, there is already an SSL socket without my determination. ssl::context is needed to choose the security method for the secure channel. there are SSL 2 & 3 and TLS 1 & 2 and even client and server specifications if you know that you are a client or server.
I used the definition BOOST_ASIO_ENABLE_OLD_SSL for building an application if you have issues, try to enable it if it exists.
Secondly, we need to establish a connection with the server:
void SMTPClient::connect(const std::string& hostname, unsigned short port) {
boost::system::error_code error = boost::asio::error::host_not_found;
boost::asio::ip::tcp::resolver resolver(service_);
boost::asio::ip::tcp::resolver::query query(hostname, boost::lexical_cast<std::string>(port));
boost::asio::ip::tcp::resolver::iterator endpoint_iterator = resolver.resolve(query);
boost::asio::ip::tcp::resolver::iterator end;
while (error && endpoint_iterator != end) {
socket_.close();
socket_.connect(*endpoint_iterator++, error);
}
if (error) {
std::cerr << "error: " << error.message());
} else {
handshake();
}
}
tcp::resolver performs forward resolution of a query to a list of entries and afterward, we connect to those queries.
The stage describes how to establish a secure channel.
void SMTPClient::handshake() {
// Receiving the server identification
std::string serverName = socket_.remote_endpoint().address().to_string();
if (tlsEnabled_) {
boost::system::error_code error;
tlsEnabled_ = false;
serverId_ = read(Ready);
message_ << "EHLO " << serverName << "\r\n";
write();
read();
message_ << "STARTTLS" << "\r\n";
write();
read(Ready);
sslSocket_.handshake(SslSocket::client, error);
if (error) {
std::cerr << "Handshake error: " << error.message());
}
tlsEnabled_ = true;
} else {
serverId_ = read(Ready);
}
message_ << "EHLO " << serverName << "\r\n";
write();
read();
}
Following the establishment of an insecure connection we receive the “ready” code from the server it is a signal to start the negotiation.
Following “STARTTLS” we require to perform a handshake.
After establishing a secure connection we again need to repeat the initial “EHLO” to get all available services from the SMTP Server.
During the course of an SSL handshake, the client and server together will do the following:
An SSL handshake involves multiple steps, as the client and server exchange the information necessary for completing the handshake and making further conversation possible.
The authentication step may not be present at your connection.
There are two authentication protocols - PLAIN and LOGIN. The difference is how to send credentials to the server. A login and a password are sent separately in two different messages or together.
void SMTPClient::connect(
const std::string& hostname,
unsigned short port,
const std::string& username,
const std::string& password,
AuthenticationProtocol protocol) {
connect(hostname, port);
switch (protocol) {
case LOGIN:
authLogin(username, password);
break;
case PLAIN:
authPlain(username, password);
break;
default:
break;
}
}
void SMTPClient::authPlain(const std::string& user, const std::string& password) {
std::string auth_hash = base64_encode('\000' + user + '\000' + password);
message_ << "AUTH PLAIN\r\n";
write();
read(Accepted);
message_ << auth_hash << "\r\n";
write();
read(AUTH_successful);
}
void SMTPClient::authLogin(const std::string& user, const std::string& password) {
std::string user_hash = base64_encode(user);
std::string pswd_hash = base64_encode(password);
message_ << "AUTH LOGIN\r\n";
write();
read(Accepted);
message_ << user_hash;
write();
read(Accepted);
message_ << pswd_hash;
write();
read(AUTH_successful);
}
Finally, we start to implement a mechanism actually email sending to someone.
We need to add all sender, recipients and message body:
void SMTPClient::send(const Mail& mail) {
newMail(mail);
recipients(mail);
body(mail);
}
Adding a sender:
void SMTPClient::newMail(const Mail& mail) {
const Mail::User& sender = mail.getSender();
message_ << "MAIL FROM: <" << sender.address() << ">\r\n";
write();
read();
}
Adding recipients:
void SMTPClient::recipients(const Mail& mail) {
const Mail::Recipients& recipients = mail.getRecipients();
Mail::Recipients::const_iterator it = recipients.begin();
for (; it != recipients.end() ; ++it) {
message_ << "RCPT TO: <" << it->address() << ">\r\n";
write();
read();
}
}
The information above describes a real sender and receivers however we again add that information to the body method (see below) and it is visible information in an email.
Consequently, we can send an email to “A” but in the email to show "B" and even to change the sender.
Adding a body:
void SMTPClient::body(const Mail& mail) {
// Notify that we're starting the DATA stream
message_ << "DATA\r\n";
write();
read(Data);
// Setting the headers
const Mail::User& sender = mail.getSender();
addresses("To: ", mail);
address("From: ", sender);
// Send the content type if necessary
const std::string& contentType = mail.getContentType();
if (not contentType.empty()) {
message_ << "MIME-Version: 1.0\r\n";
message_ << "Content-Type: " << contentType << "\r\n";
}
// Send the subject
message_ << "Subject: " << mail.getSubject() << "\r\n";
// Send the body and finish the DATA stream
message_ << mail.getBody() << "\r\n.\r\n";
write();
read();
}
last but not least 😀
tlsEnable_ is a flag whether we need a secure connection.
void SMTPClient::write() {
boost::system::error_code error;
const std::string str = message_.str();
const boost::asio::const_buffers_1 buffer = boost::asio::buffer(str);
if (tlsEnabled_) {
sslSocket_.write_some(buffer, error);
} else {
socket_.write_some(buffer, error);
}
if (error) {
std::cerr << "error: " << error.message();
}
message_.str(std::string());
}
We use a buffer with a limit of 256; it is enough for messages.
std::string SMTPService::read(RessultCode expectedReturn) {
boost::array<char, 256> buffer;
std::size_t bytesReceived = 0;
try {
if (tlsEnabled_) {
bytesReceived = sslSocket_.read_some(boost::asio::buffer(buffer));
} else {
bytesReceived = socket_.receive(boost::asio::buffer(buffer));
}
} catch (const std::exception& ex) {
std::cerr << "Exception: " << ex.what();
}
std::string answer;
if (bytesReceived == 0) {
std::cerr << "The server closed the connection.";
}
std::copy(buffer.begin(), buffer.begin() + bytesReceived, std::back_inserter(answer));
unsigned short returnValue = atoi(answer.substr(0, 3).c_str());
if (static_cast<unsigned short>(returnValue) != expectedReturn) {
std::cerr << "Expected answer status to be " << expectedReturn << ", received " << answer);
}
return answer;
}
It is one of the easiest ways how to send emails. If your project allows using an external library you may use