Ratchet Library :: SMTP Protocol Implementation
API  ·  Manual

ratchet.smtp.client API Reference
ratchet.smtp.server API Reference


Initialization and "Handshake"

The SMTP client can be used to control arbitrary connections to a mail transfer agent (MTA). Once a connection has been established, create a ratchet.smtp.client object to get started.

-- Initialize "socket"...

local client = ratchet.smtp.client.new(socket)

Once the object has been created, the first thing to do is generally read the banner from the remote server:

local banner = client:get_banner()
assert(banner.code == "220")
print("Server says: " .. banner.message)

Next, a client must identify itself to the server with an EHLO or HELO command. If the server does not accept a EHLO command first, the client should try the HELO command.

local ehlo = client:ehlo("my.hostname.com")
if ehlo.code:sub(1, 1) == "5" then
    local helo = client:helo("my.hostname.com")
    assert(helo.code == "250")

After the server responds to the EHLO/HELO commands, the client also knows which extensions the server supports. You can manually check for extensions, like STARTTLS, like so:

if client.extensions:has("STARTTLS") then
    local starttls = client:starttls()
    if starttls.code == "250" then
        -- Encrypt the socket and re-EHLO...

At this point, the client is initialized and has completed the handshake.

Mail Transactions

A single SMTP session can handle multiple mail transactions to a server. Each mail transaction will begin with a MAIL FROM command, which also specifies the mail sender address. If a server supports the PIPELINING extension, you cannot immediately check this command's reply like we have been doing so far. So, for now, we'll just queue up all the message envelope commands:

local mailfrom = client:mailfrom("sender@slimta.org", #message_data)
local rcptto1 = client:rcptto("rcpt1@slimta.org")
local rcptto2 = client:rcptto("rcpt2@slimta.org")

When we're done sending the MAIL FROM command and all RCPT TO commands, we can tell the server that we are about to begin sending message data:

local data = client:data()

Only at this point in the mail transaction will all the mail transaction command replies be populated. We should check them for any errors:

assert(mailfrom.code == "250")
assert(rcptto1.code == "250")
assert(rcptto2.code == "250")
assert(data.code == "354")

If you're being robust and checking reply codes without the assert() function, you may encounter a case where the server rejects the MAIL FROM or all the RCPT TOs and yet still returns a "354" when you send the DATA command. In this case, you should only send an empty message data:

local send_data = client:send_empty_data()
assert(send_data.code == "250")

Otherwise, if the message envelope was successful, you can send the message data as usual:

local send_data = client:send_data(message_data)
if send_data.code ~= "250" then

At this point, you can safely start another message transaction, or quit the session with client:quit().


The SMTP server library allows an application to receive connections and messages and act upon them. This could be useful when writing an MTA, MDA, or other mail-related application.

Because logic and policies can take place at any point during an SMTP session, the server library is designed in such a way that incoming SMTP commands from the client are handled with the minimum logic required by the RFC, and additionally calling a handler function supplied by the application to do any custom things and alter the reply code and/or message.

The first step is creating a table of handler methods. This table should have methods whose names are the upper-case simple command names, for example:

local custom_handlers = {}

function custom_handlers:BANNER(reply)
    reply.code = "220"
    reply.message = "Welcome to ratchet ESMTP"

function custom_handlers:EHLO(reply, ehlo_as)
    reply.code = "250"
    reply.message = "Hello, " .. ehlo_as

-- etc. etc.

Notice the first parameter (after from the implicit self) in each method is given as reply. This contains the current reply that will be sent to the client when the method returns. You can overwrite the code, message, and enhanced_status_code attributes of this table, but do not overwrite reply with a new table as it will not be picked up properly.

Standard SMTP commands are supported, most with custom arguments passed in to handler methods. Here is a simple list of built-in commands and the extra arguments given to handler methods:

  • BANNER: no arguments
    default reply: 220 ESMTP ratchet SMTP server N.NN.NN
  • EHLO: ehlo_as -- The EHLO string provided by the client.
    default reply: 250 Hello *ehlo_as*
  • EHLO: helo_as -- The HELO string provided by the client.
    default reply: 250 Hello *helo_as*
  • STARTTLS: extensions -- Supported ESMTP extensions, for possible modification.
    default reply: 220 2.7.0 Go ahead
  • MAIL: address -- The sender address provided by the client.
    default reply: 250 2.1.0 Sender <address> Ok
    oversized message reply: 552 5.3.4 Message size exceeds NNNN limit
  • RCPT: address -- The recipient address provided by the client.
    default reply: 250 2.1.5 Recipient <address> Ok
  • DATA: no arguments
    default reply: 354 Start mail input; end with <CRLF>.<CRLF>
  • RSET: no arguments
    default reply: 250 Ok
  • NOOP: no arguments
    default reply: 250 Ok
  • QUIT: no arguments
    default reply: 221 Bye

Additional, arbitrary commands can be supported by adding upper-case methods to the handler table. Any key in the handler table that is completely upper-case will be recognized as an SMTP command, so be careful.

Once a connection to the server has been established and a socket has been created by accept(), create a ratchet.smtp.server object and then completely handle an SMTP session like so:

local server = ratchet.smtp.server.new(socket, custom_handlers)

Notice that the handle() method does not take any arguments or return any values; you should use your own custom handle table logic to control the inputs and outputs of the SMTP server.

Last modified:  Sun, 17 Aug 2014 09:32:32 -0400
Author:  ian.good