Instant Mail Notifications Using Node.js

As a user of Dispostable myself I don't like waiting for the inbox page to refresh automatically (every minute or so) or manually checking for new messages. I want to be notified instantly as soon as the message I'm waiting for has arrived! In this post Ill show a simplified version of how we implemented this for Dispostable.

There are several ways you can notify the user, the first which is polling often using ajax, giving the illusion of being instant (for example every few seconds). This is very inefficient and still, the "worst" case scenario is an x second delay. In addition you will have to keep track of all the incoming messages and hit your database to see if a new message has arrived for that particular inbox. With the tens of thousands plus emails we get every day this will be very inefficient.

So pretty much the best way to do this is ajax longpolling; open a connection with the server once and wait till the server notifies us. There are several libraries available to do this but I decided to use node.js. Node.js is perfect for longpolling as it's entirely event based. As long as we have an open connection with a client while there is no activity (events), we can handle tens of thousands of clients at the same time.

The simple example on the node.js website gives a good overview of how node works. After you connect with the server you will see the "Hello World" message after a 2 second delay, while in the mean time still being available for new clients; it's non-blocking.

 var sys = require('sys'), 
    http = require('http');
 http.createServer(function (req, res) {
   setTimeout(function () {
     res.writeHead(200, {'Content-Type': 'text/plain'});
     res.write('Hello World');
     res.close();
   }, 2000);
 }).listen(8000);
 sys.puts('Server running at http://127.0.0.1:8000/');
 

This is exactly what we needed with the email notifications. You connect to the server (e.g. http://127.0.0.1:8000/myinbox) and then wait till the server lets us know the new message has arrived. Getting the inbox from the url is also simple and is provided in the request object 'req':

 var inbox = req.url.substr(1); // Strip the preceding '/'
 

Since our mailserver isn't written in node.js it needs a way to communicate what new messages have arrived for what inbox. We therefore need another small tcp server which only handles the connection with the mailserver.

 tcp.createServer(function (socket) {
     socket.addListener('data', function(data) {
       // A new message has arived
       notifyUser(data);
     });
    }
 ).listen(8001, 'localhost');
 

Now the only missing piece is the letting the waiting client know a new message has arrived. For this example we'll use an EventEmitter. This allows us to "subscribe" to a certain event, and easily notify the subscriber:

 var hub = new EventEmitter();
 
 // Subscribing / listening
 hub.addListener('myevent', function () {
   puts('myevent occured!');
 }
 
 // Notifying / emitting
 hub.emit('myevent');
 

Combining the http and tcp server with the EventEmitter will result in the following code:

 var sys = require('sys'),
     tcp = require('tcp'),
    http = require('http'),
    events = require('events');
 
 var hub = new events.EventEmitter();
 
 tcp.createServer(function (socket) {
     socket.addListener('data', function(data) {
       // Telnet adds \r\n, so strip it
       var inbox = data.substr(0, data.length - 2);
       sys.puts('New message for: ' + inbox);
       hub.emit(inbox);
     });
    }
 ).listen(8001, 'localhost');
 sys.puts('Tcp server running at 127.0.0.1:8001');
 
 http.createServer(function (req, res) {
   var inbox = req.url.substr(1);
   sys.puts('Client waiting for: ' + inbox);
 
   hub.addListener(inbox, function () {
     res.writeHead(200, {'Content-Type': 'text/plain'});
     res.write('New mail for: ' + inbox);
     res.close();
   });
 }).listen(8000);
 sys.puts('Server running at http://127.0.0.1:8000/');
 

All you have to do now is point your browser to http://127.0.0.1:8000/test. Then telnet to 127.0.0.1 port 8001 and type test. As soon as you press enter your browser will show the page!

Note that this is a very naive implementation just to give a quick overview of how this can be done :). If you want to use this for your own projects it would be best to use your own custom "EventEmitter". Also make sure you handle errors, signals (SIGPIPE for example) and events like 'close' where you remove the listener.