Skip to content

Instantly share code, notes, and snippets.

@JamesRyanATX
Created June 25, 2010 22:37
Show Gist options
  • Save JamesRyanATX/453550 to your computer and use it in GitHub Desktop.
Save JamesRyanATX/453550 to your computer and use it in GitHub Desktop.
/**
*
* smtpd.js -- an event-driven simple SMTP service for Node.js
*
* smtpd.js fires events at various points in the lifecycle of an SMTP
* connection, and those events are caught by observer functions added via
* SMTPD.Event.observe(). The design is very similar to Prototype.js's
* event observation model.
*
* Observers can also fire custom events, which can be caught by other custom
* observers.
*
* Example: read a message as soon as it's been accepted for delivery
*
* SMTPD.Event.observe('accept', function(session) {
* alert('Sweet, an email from ' + session.message.from);
* });
*
*
* Example: only accept emails to certain domains, where checkDeliverability()
* is a method that returns false if the message cannot be accepted.
*
* SMTPD.Event.observe('to', function(session) {
* if (!checkDeliverability(session.message.to)) {
* session.send('550 non-existent inbox')
* session.close();
* }
* });
*
*
* These events are observable:
*
* connect - When a connection is established
* from - When the client submits the MAIL FROM directive
* to - When the client submits the RCPT TO directive
* accept - When a message is submitted for delivery
*
*
* Internal events (not recommended for observation)
*
* data - When anything is received from the client
* queue - When a message is queued, before "accept"
*
*
* TODO:
* - multiple messages per session
* - validate and sanitize to and from addresses
*
*
* Copyright (c) 2010 James Ryan and released under the MIT license
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
**/
var net = require('net');
var sys = require('sys');
SMTPD = {
Config: {
PublicHostname: 'smtp.mywebsite.com',
Appname: 'mywebsite.com',
Port: 10025,
Hostname: 'localhost'
},
EOL: "\r\n",
Codes: {
UNRECOGNIZED_COMMAND: 502,
SERVICE_READY: 220,
CLOSE: 221,
OK: 250,
START_MAIL: 354
}
}
SMTPD.Server = {
up: function() {
SMTPD.Server.instance = net.createServer(function(socket) {
new SMTPD.Session(socket);
}).listen(SMTPD.Config.Port, SMTPD.Config.Hostname);
},
down: function() {
}
}
SMTPD.Event = {
observers: { },
observe: function(event, observer) {
if (typeof SMTPD.Event.observers[event] === 'undefined') SMTPD.Event.observers[event] = [ ];
SMTPD.Event.observers[event].push(observer);
},
fire: function(event, session, data, opt) {
opt = (typeof opt === 'undefined') ? { } : opt;
var observers = SMTPD.Event.observers[event] || [ ];
var accepted = 0
if (observers.length > 0) {
for (var i = 0; i < observers.length; i += 1) {
accepted += observers[i].call(session, data) ? 1 : 0
}
}
if (!accepted && opt.onNoResponse)
opt.onNoResponse();
}
}
// HELO
SMTPD.Event.observe('data', function(data) {
if (this.queueData || !data.match(/^HELO\s*/i)) return false;
this.message.client = this.filterCommand(data, 'helo');
this.send(SMTPD.Codes.OK + ' ' + SMTPD.Config.PublicHostname + ' says howdy, ' + this.socket.remoteAddress);
return true;
});
// EHLO
SMTPD.Event.observe('data', function(data) {
if (this.queueData || !data.match(/^EHLO\s*/i)) return false;
this.send(SMTPD.Codes.OK + '-' + SMTPD.Config.PublicHostname + ' says howdy, ' + this.socket.remoteAddress);
this.send(SMTPD.Codes.OK + ' 8BITMIME');
return true;
});
// QUIT
SMTPD.Event.observe('data', function(data) {
if (this.queueData || !data.match(/^QUIT\s*/i)) return false;
this.close();
return true;
});
// MAIL FROM
SMTPD.Event.observe('data', function(data) {
if (this.queueData || !data.match(/^MAIL FROM\s*/i)) return false;
this.message.from = this.filterCommand(data, 'mail from');
this.send(SMTPD.Codes.OK + ' ok');
this.fire('from');
return true;
});
// RCPT TO
SMTPD.Event.observe('data', function(data) {
if (this.queueData || !data.match(/^RCPT TO\s*/i)) return false;
this.message.to = this.filterCommand(data, 'rcpt to');
this.send(SMTPD.Codes.OK + ' ok');
this.fire('to');
return true;
});
// End of DATA
SMTPD.Event.observe('data', function(data) {
if (!this.queueData || !data.match(/^\.\s*/i)) return false;
this.send(SMTPD.Codes.SERVICE_READY + ' message queued for delivery');
this.fire('queue');
this.close();
return true;
});
// Append DATA
SMTPD.Event.observe('data', function(data) {
if (!this.queueData || data.match(/^\.\s*/i)) return false;
this.message.data += data
return true;
});
// DATA
SMTPD.Event.observe('data', function(data) {
if (this.queueData || !data.match(/^DATA\s*/i)) return false;
this.message.data = '';
this.queueData = true;
this.send(SMTPD.Codes.START_MAIL + " Terminate with line containing only '.'");
return true;
});
// Connect
SMTPD.Event.observe('connect', function(data) {
this.send(SMTPD.Codes.OK + ' ' + SMTPD.Config.PublicHostname + ' ESMTP ' + SMTPD.Config.Appname );
return true;
});
// Submitted when a message is queued for delivery
SMTPD.Event.observe('queue', function(data) {
// Placeholder for sanitization
// ...
this.fire('accept');
this.message = { };
return true;
});
SMTPD.Session = function(socket) {
this.socket = socket;
this.socket.setEncoding("utf8");
this.addListeners();
}
SMTPD.Session.prototype = {
message: { },
fire: function(event, data, opt) {
SMTPD.Event.fire(event, this, data, opt);
},
send: function(data) {
this.socket.write(data + SMTPD.EOL);
},
close: function() {
this.send(SMTPD.Codes.CLOSE + ' ' + SMTPD.Config.PublicHostname + ' closing connection' );
this.socket.end();
},
filterCommand: function(data, command) {
return data.replace(new RegExp('^' + command + ' *:{0,1} *', 'i'), '').replace(SMTPD.EOL, '');
},
onConnect: function() {
this.fire('connect');
},
onData: function(data) {
var self = this;
this.fire('data', data, {
onNoResponse: function() {
self.send(SMTPD.Codes.UNRECOGNIZED_COMMAND + ' Unrecognized command');
}
});
},
onClose: function() {
// cleanup work
},
addListeners: function() {
var self = this;
this.socket.addListener("connect", function () {
self.onConnect.call(self);
});
this.socket.addListener("data", function(data) {
self.onData.call(self, data);
});
this.socket.addListener("end", function(data) {
self.onClose.call(self, data);
});
}
}
SMTPD.Server.up();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment