A few months ago, I wrote an SMTP server from scratch to learn about the protocol. It soon came to me that the knowledge of SMTP internals is not as common as it should be. Upon not finding a satisfactory blog on the topic, I read through the RFCs and tried to create my own server. I am writing this blog as a relatively concise summary of that.
Before we go on to writing our server, I want to give some context that should be treated as a prerequisite.
SMTP is the protocol that is used to send and receive emails (as the name suggest, Simple Mail Transfer Protocol). When a mail is sent by a user, it is handed over to an SMTP server to pass it on to its destination, which is a receiving SMTP server. Once the mail reaches its destination SMTP server, other protocols such as IMAP or POP3 take over and store the emails so that they can be retrieved by the receiving user at a later point.
SMTP uses stateful, long-lasting TCP connections where the client and server send data back and forth. The client can even send multiple emails on the same connection. This is in contrast to HTTP 1, where client asks for exactly one resource and the server sends back just that before the connection is closed.
The SMTP specification defines some commands that the client can use to set the headers and content. The client will send these commands in their specified format with arguments, and the server will respond with a response code that the client can understand. Optionally, the server can send some human-readable data that may be useful for debugging.
These are some of the basic commands that all SMTP servers should support:
- HELO: Hello
- Once the TCP connection is established, this is always the first command that the client should send. By this command, the client identifies itself with a domain name (an FQDN - fully qualified domain name)
- Syntax:
HELO kausm.in
- MAIL FROM: I want to send an email, and this email is from …
- This starts a mail transaction (I want to send a mail), and specifies the sender address (and this email is from).
- Syntax:
MAIL FROM: [email protected]
- RCPT TO: A recipient of this email is …
- This is used to specify a recipient of this mail. If a client wants to send a mail to multiple receiving addresses, it should run this command multiple times with each of the receiving email addresses.
- Syntax:
RCPT TO: [email protected]
- DATA: My mail data is …
- Starts the actual mail data. Once this command is run and an OK response is received from the server, the client should
go on to send first the email headers and the email content as text. The delimiter to end this text is
<CRLF>.<CRLF>
. - Headers can be specified like
Date: Mon, 14 February 2022 20:40:34
,Reply-To: [email protected]
, etc. separated by newlines - Syntax:
- Client:
DATA
- Server:
250 OK
- Client:
Date: Mon, 14 February 2022 20:40:34 From: [email protected] To: [email protected] Subject: Hello Receiver Reply-To: [email protected] Hey there .
- Client:
- Starts the actual mail data. Once this command is run and an OK response is received from the server, the client should
go on to send first the email headers and the email content as text. The delimiter to end this text is
- QUIT: That’s all I wanted to do, thanks for your service.
- This tells the server that its work is done and that the server can now close the connection.
- Note that the SMTP specification explicitly mentions that the server shoudln’t close the connection abruptly unless the client executes this command. Even after you complete a mail transaction, this gives you the option to send addiotional mails over the same connection.
- Syntax:
QUIT
There are some more commands that can be used once the client and server agree that both support that command. For example,
the LOGIN
command for authentication and STARTTLS
for encryption. These are extensions that do not have to be implemented
by all SMTP servers, but are practically required to go past the security/spam filters of sophisticated receiving servers.
The functionality of these commands can be agreed upon by using an EHLO
(Extended Hello) command instead of the usual
HELO
command, which the server responds to with a list of supported extensions.
OK, let’s start.
Here’s what we need to do:
- Write a server that can accept and read from TCP connections.
- Support the
HELO
command. - Support
MAIL FROM
. - Support
RCPT TO
. - Support
DATA
.
1. Writing a TCP server
|
|
Initialize a listener on the host and port, and accept connections on it. See Go standard documentation of the net pkg for more information.
|
|
In handleConnection
, we’ll first read from the connection into a buffer and then write that back into the connection.
|
|
Now we can test out the server with netcat.
|
|
Great, we now have our TCP listener. We can get started with the SMTP commands.
2. Supporting SMTP commands
To support different commands, we need to read and parse the line input by the client. Let’s make a list of what we’ll need to do:
- Parse the text - split it into command and arguments
- If the command is a valid command, pass the
conn
object to its handler until it finishes its job and returns it back. - Sometimes, we need certain prerequisite data from commands before we can start executing other commands. For example,
the client has to send the
HELO
/EHLO
command before being able to send any other command, theMAIL FROM
needs to be run beforeRCPT TO
is run, and so on. For this reason, we have to keep state of what data we have received from the client.
The HELO
command
|
|
- RFCs: RFC 883, etc.