In this guide, we will learn about mail servers and how to create one from scratch. We will go through the intricacies it takes for sending and receiving e-mails, understand what DNS lookup is, how SMTP server and the underlying protocol works and so much more. The curiosity for this blog came from this video by Piyush Garg. Highly recommended. Now without further wait, let’s begin.
But what even is a mail server?
Simply put, it’s just a server that facilitates sending and receiving of emails via the SMTP protocol.
Let us first see how does an SMTP server work.
An email client like Gmail or Outlook connects to the local SMTP server and submits the data.
The local SMTP server adds information such as message boundaries, timestamps and formats the message in accordance with SMTP standards.
The local SMTP server then looks up the receiver’s email domain via DNS and finds out the mail server for that domain.
The SMTP server opens a connection with the receiver’s mail server and transfers the email.
The SMTP server on the receiver’s end receives the email and processes it before routing it over to the receiver’s inbox.
Using protocols like POP3 and IMAP, the receiver can fetch their emails in their inbox.
Now would be the perfect time to learn some key SMTP commands. These can be seen as the equivalent to HTTP commands like PUSH and GET and they are paramount in establishing an SMTP connection between two servers.
SMTP Commands
HELO - The server introduces itself and initiates the SMTP connection.
EHLO - It is an advanced version of HELO
RCPT TO - This command indicates the recipient’s email address.
MAIL FROM - This command indicates the sender’s email address.
DATA - This command initiates transfer of the email contents.
QUIT - This command closes the SMTP connection.
Before we move on to the code, it is of utmost importance to be vary with a few more technical terms.
Some Key Terms
DNS Server - It is a device that translates domain names to their IP addresses. For example, take helloworld.com. A DNS server has the capability to convert it to let’s say 72.124.110.234. Beyond this, a DNS server allows network devices to communicate using IP addresses.
Address record - Also commonly called an A-record, this determines which IP address belongs to a domain name.
Mail Exchanger - Also known as a DNS MX record, this record is used to tell the computers which mail servers accept the incoming mail for your domain and where emails sent to your domains should be routed to.
Now that we have come across the fundamentals, it’s time to get our hands dirty.
const SMTPServer = require("smtp-server").SMTPServer;
const server = new SMTPServer({
allowInsecureAuth: true,
authOptional: true,
onConnect(session, cb){
console.log('onConnect', session.id)
cb()// accepted
},
onMailFrom(address, session, cb){
console.log('onMailFrom', address.address, session.id)
cb()
},
onRcptTo(address, session, cb){
console.log(`onRcptTo`, address.address, session.id)
},
onData(stream, session, cb){
stream.on('data', (data)=>{
console.log(`onData ${data.toString()}`)
})
stream.on('end', cb)
}
})
server.listen(25, ()=>{
console.log("The server is running on port 25")
})
Let us see what we are doing here
First of all, we install the smtp-server package and the nodemon package from npm to help ease our development process.
We then declare a const named SMTPserver which will require the downloaded module. We will need this class to create an SMTP server instance. Upon that creation of which we can pass an object as a configuration.
Now since we are not making a production grade server that has authentication, we need a workaround for the auth so that our mail server works (for now) without needing any auth. The allowInsecureAuth: true and authOptional: true allow us to achieve the same.
Now we use the onConnect(), which is a callback function that runs when a client connects to a server. The cb() is also a callback function that signals whether our connection has been accepted or not. The session is an object that contains details about the connection.
The next few lines are a console log of the session and calling of the callback function - indicating that the callback function to accept the connection.
Now comes the onMailFrom() function which is triggered when the client specifies the sender’s mail address. The arguments it has are the address, the session ID and a callback function to accept or reject the sender.
The next callback i.e. the onRcptTo() is then triggered when the client specifies a recipient’s email address. The address.address contains the recipient’s email.
The next line is a console.log statement that logs the recipient’s email address and the session ID when the RCPT TO command is received.
Now comes the onData(stream, session, cb) part which is triggered when the client sends the email content, and the data of the e-mail is streamed as a Readable Stream object.
The next line sets up a listener on the stream that gets triggered when the email content is received. After which, it is logged in to the console by converting it to string so that it is in a readable format.
The next line listens for the end of the data stream and once that happens, the callback cb() is called to indicate that all of the data transfer is complete. The last few lines are specifying the port on which our SMTP server runs which is 25 and a console log statement indicating the same.
Conclusion
This is a basic implementation of an SMTP server that accepts incoming connection, logs the sender’s and the recipient’s email address, logs the email content and is set to allow to run on insecure connections without needing anu authentication, mainly used for testing environments. In a more true-to-life scenario, we would need to make it more secure and make authentication mandatory to verify the integrity and sovereignty of emails sent to and from the server.