Overview

This simple tutorial demonstrates how Mongoose Library can be used to implement an SMTP client over TLS.

SMTP in a nutshell

SMTP was originally a very simple protocol, hence its name (Simple Mail Transport Protocol), commands are four-letter human readable words and responses are three-digit codes, followed by a human readable text. It even has a built-in HELP command !

Focusing on the mail transfer, we find the following states or phases:

  • The client connects, the server greets with a 220 code
  • The client sends HELO and its name, the server responds 250
  • The client sends MAIL followed by FROM: and its email address, the server responds 250 again
  • The client sends RCPT followed by TO: and the recipient email address, the server responds 250 again
  • The client sends DATA, the server responds 354
  • The client sends the mail body, and ends it with a dot . on a line by itself, the server responds with the 250 code once more
  • The client sends QUIT, the server responds 221 and the session is finished

Due to the necessity of having proper authentication, ESMTP (for Enhanced SMTP) was introduced. The client now starts the dialog by sending EHLO instead. The server will then respond with a list of capabilities, among which there'll be those we need to start a TLS session and exchange authorization credentials.

TLS and authentication

Now focusing on establishing a TLS session and authenticate to the server, we find the following states or phases:

  • The client sends EHLO and its name, the server responds 250 and a list of its cababilities.
  • The client sends STARTTLS, the server responds with a 220 code and the TLS handshake starts
  • The server sends its certificate and the client validates it against a CA certificate
  • The client sends EHLO and its name again, the server responds 250 and a list of its cababilities at this stage
  • The client chooses an authentication method and sends AUTH, followed by the chosen method and any credentials; the server responds with a 235 code and the client may proceed with the mail transfer

Main function

In the main() function we initialize an event manager, start our client, and start the event loop.

  • The client connection is created by calling mg_connect() and passing it a pointer to the URL, and a pointer to the event handler function, with an optional argument.

Event handler

The event handler function is called by the event manager every time there is an event to be handled, here we will use:

  • MG_EV_OPEN: The connection has been created
  • MG_EV_READ: There is outstanding data, received from the socket. We will mainly drive here our state machine
  • MG_EV_TLS_HS: The TLS handshake has been done, we resume our processing
  • MG_EV_CLOSE: The connection has closed, we just set a variable so the main loop exits

The SMTP client evolves through a series of states. We use c->label, an auxiliary buffer, to hold the client state; it is initialized to zero at startup so that will be our first state. Those are the following:

  • EHLO: We have just received the server greeting and send the EHLO command, our next state, once the server responds, will be STARTTLS
  • STARTTLS: The server has just sent its list of capabilities. We send the STARTTLS command and wait for its confirmation
  • STARTTLS_WAIT: The server has just sent its confirmation; so we initialize TLS by calling mg_tls_init(). Our next state will be AUTH. Once the TLS handshake is done, we'll get an MG_EV_TLS_HS event; then we'll send EHLO again to restart the initial handshake and get the server capabilities, among them those belonging to authentication.
  • AUTH: The server has just sent its list of capabilities. We send the AUTH command along with the chosen authentication method and proper credentials, and wait for its confirmation
  • FROM, TO, DATA, BODY: Once the server confirms, we send the rest of the pertaining commands along with proper data, in order to send the mail. After sending the mail body, we send an extra line containing only a dot and the end of line sequence.
  • QUIT, END: Once the server acknowledges receipt of the mail to be sent, we send the QUIT command, and after we get its confirmation we request Mongoose to drain its output buffer and close the connection when it has finished.

Note that this client is very simple and does not do any form of error checking neither at the SMTP level nor at the connection level. For the latter, check the error handling tutorial.

Build and run

The makefile will take care of building with TLS support, and it defaults to using mbedTLS. If you need to use openSSL, just pass an additional argument to make: SSL=OPENSSL.

Before trying this example, open main.c and change the email server, sender, and recipient addresses.

  • If you've not already done so, clone the Mongoose Library repo
    $ git clone https://github.com/cesanta/mongoose
    
  • Build and run the example:
    $ cd mongoose/examples/smtp-client
    $ make clean all
    
  • Observe the log
    26f6264f 2 main.c:21:fn                 <-- 220-domain.com ESMTP Exim 4.95 #2 Wed, 28 Sep 2022  12:24:38 -0500 
    26f6264f 2 main.c:62:fn                 --> EHLO myname
    26f626fe 2 main.c:21:fn                 <-- 250-domain.com Hello myname [ip address]
    250-SIZE 52428800
    250-8BITMIME
    250-PIPELINING
    250-PIPE_CONNECT
    250-AUTH PLAIN LOGIN
    250-STARTTLS
    250 HELP
    26f626ff 2 main.c:62:fn                 --> STARTTLS
    26f627a6 2 main.c:21:fn                 <-- 220 TLS go ahead
    26f627a7 2 main.c:62:fn                 --> 
    26f62905 2 main.c:67:fn                 TLS handshake done! Sending EHLO again
    26f629ab 2 main.c:21:fn                 <-- 250-domain.com Hello myname [ip address]
    250-SIZE 52428800
    250-8BITMIME
    250-PIPELINING
    250-PIPE_CONNECT
    250-AUTH PLAIN LOGIN
    250 HELP
    26f629ab 2 main.c:62:fn                 --> AUTH PLAIN kjHKJHkjhkJHkjhaWxlQHdlYnRvcklhgkjggKLJGIUYGIK==
    26f63228 2 main.c:21:fn                 <-- 235 Authentication succeeded
    26f63228 2 main.c:62:fn                 --> MAIL FROM: <aaa@domain.com>
    26f632cd 2 main.c:21:fn                 <-- 250 OK
    26f632cd 2 main.c:62:fn                 --> RCPT TO: <bbb@domain.com>
    26f6337e 2 main.c:21:fn                 <-- 250 Accepted
    26f6337e 2 main.c:62:fn                 --> DATA
    26f63423 2 main.c:21:fn                 <-- 354 Enter message, ending with "." on a line by itself
    26f63423 2 main.c:62:fn                 --> From: My Mail Sender <aaa@domain.com>
    Subject: Test email from Mongoose library!
    
    Hi!
    This is a test message.
    Bye.
    .
    26f634c9 2 main.c:21:fn                 <-- 250 OK id=1odfhf-000PfG-Dy
    26f634c9 2 main.c:62:fn                 --> QUIT
    26f636ea 2 main.c:21:fn                 <-- 221 domain.com closing connection
    26f636ea 2 main.c:60:fn                 end
    26f636ea 2 main.c:62:fn                 --> 
    

Notes on TLS support

  • If you need to provide extra information to compile or link, like for example includes and library paths, do it with the EXTRA_CFLAGS variable, like:

    $ make clean all SSL=MBEDTLS EXTRA_CFLAGS="-I/path/to/mbedtls/include -L/path/to/mbedtls/lib"
    

    For more information on building TLS clients, check the TLS tutorial

  • The included set of CA certificates will cover major sites, but if your site certificate happens to be signed by a CA that is not included there, you'll have to get that certificate. You'll be able to tell because the example will fail right after sending STARTTLS:

    26d1ad47 2 main.c:62:fn                 --> STARTTLS
    26d1adf7 2 main.c:21:fn                 <-- 220 TLS go ahead
    26d1adfa 2 main.c:62:fn                 --> 
    26d1aead 1 mongoose.c:404:mg_error      1 0x5 TLS handshake: -0x2700
    
  • If you don't know beforehand, run a network sniffer and at the start of the certificate exchange you'll be able to see your site's certificate and those who signed it. Go the issuer/signer site and get the root of trust certificates; replace ca.pem with those.

    certificate

Notes on Google accounts

If you want to try this on a Gmail account, or a Workspace account with its own domain name that uses Google as its provider, you need to create an App Password. At the time of writing this tutorial, this requires that your account has 2-factor authentication enabled, otherwise this option won't even show in your Security menus. Once you create this App Password, you will use it with your email address and in place of your account password; that is:

static const char *server = "tcp://smtp.gmail.com:587";
static const char *user = "myusername@gmail.com";
static const char *pass = "AppPassword4this";

To create your Application Password, just follow Google's documentation