How to Implement SMTP Client on C++

Written by iichikk | Published 2022/09/21
Tech Story Tags: c++ | smtp | email | smtp-client-on-c++ | implement-smtp-client | coding | programming | learning-to-code

TLDRSimple Mail Transfer Protocol (SMTP) is an essential part of email transferring and for communication, we always need servers and clients that collaborate and negotiate for sending emails. 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. To send emails, the SMTP Client needs to connect to an SMTP Server that will send the messages to the targeted recipients. The negotiation can be divided into. three stages. The server returns a number before the response message like fail or success.via the TL;DR App

Introduction

Simple Mail Transfer Protocol (SMTP) is a widely used protocol for the delivery of emails between TCP/IP systems and users.

SMTP is an essential part of email transferring and for communication, we always need servers and clients that collaborate and negotiate for sending emails.

What is SMTP Server?

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.

What is an SMTP client?

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.

SMTP client and server negotiation

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

Realization of the first stage

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.

Realization of the second stage

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.

What happens during an SSL handshake?

During the course of an SSL handshake, the client and server together will do the following:

  • Specify which version of SSL (SSL 1, 2, 3, etc.) they will use
  • Decide on which cipher suites they will use
  • Authenticate the identity of the server via the server’s public key and the SSL certificate authority’s digital signature
  • Generate session keys in order to use symmetric encryption after the handshake is complete

An SSL handshake involves multiple steps, as the client and server exchange the information necessary for completing the handshake and making further conversation possible.

How to authorize to SMTP Server?

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);
}

Realization of the third stage

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();
}

How to write and read from a socket?

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;
}

Conclusion

It is one of the easiest ways how to send emails. If your project allows using an external library you may use libquickmail , libcurl and VMime. Depends on the library but your amount of lines may be the same.


Written by iichikk | Developer & manager with 6+ years of experience
Published by HackerNoon on 2022/09/21