Skip to content
Browse files

First real working implementation

  • Loading branch information...
1 parent 023cd9b commit 7b3a080e55536c874b24d69978cb16e12649c02a @baudehlo committed Mar 12, 2011
Showing with 1,079 additions and 0 deletions.
  1. +8 −0 README
  2. +32 −0 config.js
  3. +1 −0 config/databytes
  4. +1 −0 config/dnsbl.zones
  5. +3 −0 config/plugins
  6. +1 −0 config/smtp
  7. +111 −0 configfile.js
  8. +527 −0 connection.js
  9. +9 −0 constants.js
  10. +1 −0 haraka.js
  11. +8 −0 logger.js
  12. +139 −0 plugins.js
  13. +42 −0 plugins/dnsbl.js
  14. +13 −0 plugins/relay_all.js
  15. +23 −0 plugins/test_queue.js
  16. +82 −0 rfc1869.js
  17. +46 −0 server.js
  18. +32 −0 transaction.js
View
8 README
@@ -0,0 +1,8 @@
+Haraka - a Node.js Mail Server
+==============================
+
+Haraka is a plugin capable SMTP server. It uses a highly scalable event
+model to be able to cope with thousands of concurrent connections. Plugins
+are written in Javascript using Node.js, and as such perform extremely
+quickly.
+
View
32 config.js
@@ -0,0 +1,32 @@
+var configloader = require('./configfile');
+var path = require('path');
+var logger = require('./logger');
+
+var config = exports;
+
+var config_path = process.env.HARAKA ? path.join(process.env.HARAKA, 'config') : './config';
+
+config.get = function(name, type) {
+ var full_path = path.resolve(config_path, name);
+
+ logger.log("Loading config file: " + full_path);
+ var results;
+ try {
+ results = configloader.read_config(full_path, type);
+ }
+ catch (err) {
+ if (err.code === 'EBADF') {
+ // do nothing
+ if (type === 'ini') {
+ return configloader.empty_config(type);
+ }
+ else {
+ return null;
+ }
+ }
+ else {
+ console.log(err.name + ': ' + err.message);
+ }
+ }
+ return results;
+};
View
1 config/databytes
@@ -0,0 +1 @@
+500000
View
1 config/dnsbl.zones
@@ -0,0 +1 @@
+zen.spamhaus.org
View
3 config/plugins
@@ -0,0 +1,3 @@
+dnsbl
+relay_all
+test_queue
View
1 config/smtp
@@ -0,0 +1 @@
+port=3000
View
111 configfile.js
@@ -0,0 +1,111 @@
+// Config file loader
+
+var fs = require('fs');
+
+// for "ini" type files
+var regex = {
+ section: /^\s*\[\s*([^\]]*)\s*\]\s*$/,
+ param: /^\s*(\w+)\s*=\s*(.*)\s*$/,
+ comment: /^\s*[;#].*$/,
+ line: /^\s*(.*)\s*$/,
+ blank: /^\s*$/
+};
+
+var cfreader = exports;
+
+cfreader._config_cache = {};
+
+cfreader.read_config = function(name, type) {
+ // Check cache first
+ if (cfreader._config_cache[name]) {
+ return cfreader._config_cache[name];
+ }
+
+
+ // load config file
+ var result = cfreader.load_config(name, type);
+
+ fs.unwatchFile(name);
+ fs.watchFile(name, function (curr, prev) {
+ // file has changed
+ if (curr.mtime.getTime() !== prev.mtime.getTime()) {
+ cfreader.load_config(name, type);
+ }
+ });
+
+ return result;
+}
+
+cfreader.empty_config = function(type) {
+ if (type === 'ini') {
+ return { main: {} };
+ }
+ else {
+ return [];
+ }
+};
+
+cfreader.load_config = function(name, type) {
+
+ if (type === 'ini') {
+ result = cfreader.load_ini_config(name);
+ }
+ else {
+ result = cfreader.load_flat_config(name);
+ }
+
+ cfreader._config_cache[name] = result;
+
+ return result;
+};
+
+cfreader.load_ini_config = function(name) {
+ var result = cfreader.empty_config('ini');
+ var current_sect = result.main;
+
+ var data = new String(fs.readFileSync(name));
+ var lines = data.split(/\r\n|\r|\n/);
+
+ lines.forEach( function(line) {
+ if (regex.comment.test(line)) {
+ return;
+ }
+ else if (regex.blank.test(line)) {
+ return;
+ }
+ else if (regex.param.test(line)) {
+ var match = line.match(regex.param);
+ current_sect[match[1]] = match[2];
+ }
+ else if (regex.section.test(line)) {
+ var match = line.match(regex.section);
+ current_sect = result[match[1]] = {};
+ }
+ else {
+ // error ?
+ };
+ });
+
+ return result;
+};
+
+cfreader.load_flat_config = function(name) {
+ var result = [];
+ var data = new String(fs.readFileSync(name));
+ var lines = data.split(/\r\n|\r|\n/);
+
+ lines.forEach( function(line) {
+ var line_data;
+ if (regex.comment.test(line)) {
+ return;
+ }
+ else if (regex.blank.test(line)) {
+ return;
+ }
+ else if (line_data = regex.line.exec(line)) {
+ result.push(line_data[1]);
+ }
+ });
+
+ return result;
+};
View
527 connection.js
@@ -0,0 +1,527 @@
+// a single connection
+var config = require('./config');
+var logger = require('./logger');
+var trans = require('./transaction');
+var dns = require('dns');
+var plugins = require('./plugins');
+var constants = require('./constants');
+var rfc1869 = require('./rfc1869');
+
+var line_regexp = /^([^\n]*\n)/;
+
+var connection = exports;
+
+function setupClient(self) {
+ self.client.pause();
+ self.remote_ip = self.client.remoteAddress;
+ logger.log("connection from: " + self.remote_ip);
+ dns.reverse(self.remote_ip, function(err, domains) {
+ if (err) {
+ switch (err.code) {
+ case dns.NXDOMAIN: self.remote_host = 'NXDOMAIN'; break;
+ default: self.remote_host = 'DNSERROR'; break;
+ }
+ }
+ else {
+ self.remote_host = domains[0] || 'Unknown';
+ }
+ self.client.on('data', function (data) {
+ self.process_data(data);
+ });
+ self.client.resume();
+ self.transaction = trans.createTransaction();
+ // TODO - check for early talkers before this
+ plugins.run_hooks('connect', self);
+ });
+}
+
+function Connection(client) {
+ this.client = client;
+ this.current_data = '';
+ this.current_line = null;
+ this.state = 'cmd'; // command or data
+
+ setupClient(this);
+}
+
+exports.Connection = Connection;
+
+exports.createConnection = function(client) {
+ var s = new Connection(client);
+ return s;
+}
+
+Connection.prototype.process_line = function (line) {
+ logger.log("C: " + line);
+ if (this.state === 'cmd') {
+ this.current_line = line.replace(/\r?\n$/, '');
+ var matches = /^([^ ]*)( +(.*))?$/.exec(this.current_line);
+ var method = "cmd_" + matches[1].toLowerCase();
+ var remaining = matches[3] || '';
+ if (this[method]) {
+ try {
+ this[method](remaining);
+ }
+ catch (err) {
+ logger.log(method + " failed: " + err);
+ this.respond(500, "Internal Server Error");
+ this.disconnect;
+ }
+ }
+ else {
+ // unrecognised command
+ plugins.run_hooks('unrecognized_command', this);
+ }
+ }
+ else if (this.state === 'data') {
+ this.accumulate_data(line);
+ }
+};
+
+Connection.prototype.process_data = function (data) {
+ if (this.disconnected) {
+ logger.log("data after disconnect");
+ return;
+ }
+
+ this.current_data += data;
+
+ var results;
+ while (results = line_regexp.exec(this.current_data)) {
+ var this_line = results[1];
+ this.current_data = this.current_data.slice(this_line.length);
+ this.process_line(this_line);
+ }
+};
+
+Connection.prototype.remote_host = function() {
+ if (arguments.length) {
+ this.remote_host = arguments[0];
+ }
+ return this.remote_host;
+};
+
+Connection.prototype.remote_ip = function() {
+ if (arguments.length) {
+ this.remote_ip = arguments[0];
+ }
+ return this.remote_ip;
+};
+
+Connection.prototype.current_line = function() {
+ return this.current_line;
+};
+
+Connection.prototype.respond = function(code, messages) {
+ if (!(typeof messages === 'object' && messages.constructor === Array)) {
+ // messages not an array, make it so:
+ messages = [ '' + messages ];
+ }
+ var msg;
+ var buf = '';
+ while (msg = messages.shift()) {
+ var line = code + (messages.length ? "-" : " ") + msg;
+ buf = buf + line + "\r\n";
+ }
+
+ this.client.write(buf);
+};
+
+Connection.prototype.disconnect = function() {
+ plugins.run_hooks('disconnect', this);
+};
+
+Connection.prototype.disconnect_respond = function () {
+ this.disconnected = 1;
+ this.client.end();
+};
+
+Connection.prototype.get_capabilities = function() {
+ var capabilities = []
+
+ // TODO get AUTH mechanisms here
+ // TODO get STARTTLS here if loaded?
+
+ return capabilities;
+};
+
+Connection.prototype.reset_transaction = function() {
+ this.transaction = trans.createTransaction();
+};
+
+/////////////////////////////////////////////////////////////////////////////
+// SMTP Responses
+
+Connection.prototype.unrecognized_command_respond = function(retval, msg) {
+ switch(retval) {
+ case constants.deny: this.respond(500, msg || "Unrecognized command");
+ break;
+ case constants.denydisconnect:
+ this.respond(521, msg || "Unrecognized command");
+ this.disconnect;
+ break;
+ default: this.respond(500, msg || "Unrecognized command");
+ }
+};
+
+Connection.prototype.connect_respond = function(retval, msg) {
+ switch (retval) {
+ case constants.deny:
+ case constants.denydisconnect:
+ case constants.disconnect:
+ this.respond(550, msg || "Your mail is not welcome here");
+ this.disconnect();
+ break;
+ case constants.denysoft:
+ this.respond(450, msg || "Come back later");
+ break;
+ default:
+ this.respond(220, msg || "myhost ESMTP Haraka VER ready");
+ }
+};
+
+Connection.prototype.helo_respond = function(retval, msg) {
+ switch (retval) {
+ case constants.deny:
+ this.respond(550, msg || "HELO denied");
+ this.greeting = null;
+ this.hello_host = null;
+ break;
+ case constants.denydisconnect:
+ this.respond(550, msg || "HELO denied");
+ this.disconnect;
+ break;
+ case constants.denysoft:
+ this.respond(450, msg || "HELO denied");
+ this.greeting = null;
+ this.hello_host = null;
+ break;
+ default:
+ this.respond(250, "Haraka says hi " + this.remote_host + " [" + this.remote_ip + "]");
+ }
+};
+
+Connection.prototype.ehlo_respond = function(retval, msg) {
+ switch (retval) {
+ case constants.deny:
+ this.respond(550, msg || "EHLO denied");
+ this.greeting = null;
+ this.hello_host = null;
+ break;
+ case constants.denydisconnect:
+ this.respond(550, msg || "EHLO denied");
+ this.disconnect;
+ break;
+ case constants.denysoft:
+ this.respond(450, msg || "EHLO denied");
+ this.greeting = null;
+ this.hello_host = null;
+ break;
+ default:
+ var response = ["Haraka says hi " + this.remote_host + " [" + this.remote_ip + "]",
+ "PIPELINING",
+ "8BITMIME"
+ ];
+
+ var databytes = config.get('databytes');
+ if (databytes) {
+ response.push("SIZE " + databytes[0]);
+ }
+
+ var capabilities = this.get_capabilities();
+ var i;
+ for (i = 0; i < capabilities.length; i++) {
+ response.push(capabilities[i]);
+ }
+ this.respond(250, response);
+ }
+};
+
+Connection.prototype.quit_respond = function(retval, msg) {
+ this.respond(221, msg || "closing connection. Have jolly good day.");
+ this.disconnect();
+};
+
+Connection.prototype.vrfy_respond = function(retval, msg) {
+ switch (retval) {
+ case constants.deny:
+ this.respond(554, msg || "Access Denied");
+ this.reset_transaction();
+ break;
+ case constants.ok:
+ this.respond(250, msg || "User OK");
+ break;
+ default:
+ this.respond(252, "Just try sending a mail and we'll see how it turns out...");
+ }
+};
+
+Connection.prototype.noop_respond = function(retval, msg) {
+ switch (retval) {
+ case constants.deny:
+ this.respond(500, msg || "Stop wasting my time");
+ break;
+ case constants.denydisconnect:
+ this.respond(500, msg || "Stop wasting my time");
+ this.disconnect();
+ break;
+ default:
+ this.respond(250, "OK");
+ }
+};
+
+Connection.prototype.mail_respond = function(retval, msg) {
+ switch (retval) {
+ case constants.deny:
+ this.respond(550, msg || "mail from denied");
+ this.reset_transaction();
+ break;
+ case constants.denydisconnect:
+ this.respond(550, msg || "mail from denied");
+ this.disconnect();
+ break;
+ case constants.denysoft:
+ this.respond(450, msg || "mail from denied");
+ this.reset_transaction();
+ break;
+ default:
+ this.respond(250, msg || "sender OK");
+ }
+};
+
+Connection.prototype.rcpt_respond = function(retval, msg) {
+ switch (retval) {
+ case constants.deny:
+ this.respond(550, msg || "delivery denied");
+ this.transaction.rcpt_to.pop();
+ break;
+ case constants.denydisconnect:
+ this.respond(550, msg || "delivery denied");
+ this.disconnect();
+ break;
+ case constants.denysoft:
+ this.respond(450, msg || "delivery denied for now");
+ this.transaction.rcpt_to.pop();
+ break;
+ case constants.ok:
+ this.respond(250, msg || "recipient ok");
+ break;
+ default:
+ logger.log("No plugin determined if relaying was allowed");
+ this.respond(450, "Internal server error");
+ }
+};
+
+/////////////////////////////////////////////////////////////////////////////
+// SMTP Commands
+
+Connection.prototype.cmd_helo = function(line) {
+ var results = (new String(line)).split(/ +/);
+ var host = results[0];
+ if (!host) {
+ return this.respond(501, "HELO requires domain/address - see RFC-2821 4.1.1.1");
+ }
+
+ if (this.hello_host) {
+ return this.respond(503, "You already said HELO");
+ }
+
+ this.greeting = 'HELO';
+ this.hello_host = host;
+
+ plugins.run_hooks('helo', this, host);
+};
+
+Connection.prototype.cmd_ehlo = function(line) {
+ var results = (new String(line)).split(/ +/);
+ var host = results[0];
+ if (!host) {
+ return this.respond(501, "EHLO requires domain/address - see RFC-2821 4.1.1.1");
+ }
+
+ if (this.hello_host) {
+ return this.respond(503, "You already said EHLO");
+ }
+
+ this.greeting = 'EHLO';
+ this.hello_host = host;
+
+ plugins.run_hooks('ehlo', this, host);
+};
+
+Connection.prototype.cmd_quit = function() {
+ plugins.run_hooks('quit', this);
+};
+
+Connection.prototype.cmd_rset = function() {
+ this.reset_transaction();
+ this.respond(250, "OK");
+};
+
+Connection.prototype.cmd_vrfy = function(line) {
+ // I'm not really going to support this except via plugins
+ plugins.run_hooks('vrfy', this);
+};
+
+Connection.prototype.cmd_noop = function() {
+ plugins.run_hooks('noop', this);
+};
+
+Connection.prototype.cmd_help = function() {
+ this.respond(250, "Not implemented");
+};
+
+Connection.prototype.cmd_mail = function(line) {
+ var results;
+ try {
+ results = rfc1869.parse("mail", line);
+ }
+ catch (err) {
+ return this.respond(501, err);
+ }
+
+ this.reset_transaction();
+ var from = results.shift();
+ this.transaction.mail_from = from;
+
+ // Get rest of key=value pairs
+ var params = {};
+ results.forEach(function(param) {
+ var kv = param.match(/^(.*?)=(.*)$/);
+ if (kv)
+ params[kv[0]] = kv[1];
+ });
+
+ plugins.run_hooks('mail', this, [from, params]);
+};
+
+Connection.prototype.cmd_rcpt = function(line) {
+ if (!this.transaction.mail_from) {
+ return this.respond(503, "Use MAIL before RCPT");
+ }
+
+ var results;
+ try {
+ results = rfc1869.parse("rcpt", line);
+ }
+ catch (err) {
+ return this.respond(501, err);
+ }
+
+ var recipient = results.shift();
+ this.transaction.rcpt_to.push(recipient);
+
+ // Get rest of key=value pairs
+ var params = {};
+ results.forEach(function(param) {
+ var kv = param.match(/^(.*?)=(.*)$/);
+ if (kv)
+ params[kv[0]] = kv[1];
+ });
+
+ plugins.run_hooks('rcpt', this, [recipient, params]);
+};
+
+Connection.prototype.cmd_data = function(line) {
+ plugins.run_hooks('data', this);
+};
+
+Connection.prototype.data_respond = function(retval, msg) {
+ var cont = 0;
+ switch (retval) {
+ case constants.deny:
+ this.respond(554, msg || "Message denied");
+ this.reset_transaction();
+ break;
+ case constants.deny_disconnect:
+ this.respond(554, msg || "Message denied");
+ this.disconnect();
+ break;
+ case constants.denysoft:
+ this.respond(451, msg || "Message denied");
+ this.reset_transaction();
+ break;
+ default:
+ cont = 1;
+ }
+
+ if (!cont) {
+ return;
+ }
+
+ if (!this.transaction.mail_from) {
+ this.respond(503, "MAIL required first");
+ }
+ else if (!this.transaction.rcpt_to.length) {
+ this.respond(503, "RCPT required first");
+ }
+ else {
+ this.respond(354, "go ahead, make my day");
+ // OK... now we get the data
+ this.state = 'data';
+ }
+};
+
+Connection.prototype.accumulate_data = function(line) {
+ if (line === ".\r\n")
+ return this.data_done();
+
+ // Bare LF checks
+ if (line === ".\r" || line === ".\n") {
+ // I really should create my own URL...
+ this.respond(421, "See http://smtpd.develooper.com/barelf.html");
+ this.disconnect();
+ return;
+ }
+
+ // TODO: check size
+
+ this.transaction.data_add(line);
+};
+
+Connection.prototype.data_done = function() {
+ plugins.run_hooks('data_post', this);
+};
+
+Connection.prototype.data_post_respond = function(retval, msg) {
+ switch (retval) {
+ case constants.deny:
+ this.respond(552, msg || "Message denied");
+ this.reset_transaction();
+ break;
+ case constants.deny_disconnect:
+ this.respond(552, msg || "Message denied");
+ this.disconnect();
+ break;
+ case constants.denysoft:
+ this.respond(452, msg || "Message denied temporarily");
+ this.reset_transaction();
+ break;
+ default:
+ plugins.run_hooks("queue", this);
+ }
+};
+
+Connection.prototype.queue_respond = function(retval, msg) {
+ this.reset_transaction();
+
+ switch (retval) {
+ case constants.ok:
+ this.respond(250, msg || "Message Queued");
+ break;
+ case constants.deny:
+ this.respond(552, msg || "Message denied");
+ break;
+ case constants.denydisconnect:
+ this.respond(552, msg || "Message denied");
+ this.disconnect();
+ break;
+ case constants.denysoft:
+ this.respond(452, msg || "Message denied temporarily");
+ break;
+ default:
+ this.respond(451, msg || "Queuing declined or disabled, try later");
+ break;
+ }
+};
+
View
9 constants.js
@@ -0,0 +1,9 @@
+// Constants
+
+exports.cont = 900;
+exports.stop = 901;
+exports.deny = 902;
+exports.denysoft = 903;
+exports.denydisconnect = 904;
+exports.disconnect = 905;
+exports.ok = 906;
View
1 haraka.js
@@ -0,0 +1 @@
+var server = require('./server').createServer();
View
8 logger.js
@@ -0,0 +1,8 @@
+// Log class
+
+var logger = exports;
+
+logger.log = function (data) {
+ data = data.replace(/\n?$/, "");
+ console.log(data);
+};
View
139 plugins.js
@@ -0,0 +1,139 @@
+// load all defined plugins
+
+var logger = require('./logger');
+var config = require('./config');
+var constants = require('./constants');
+var path = require('path');
+
+var plugin_path = process.env.HARAKA ? path.join(process.env.HARAKA, 'plugins') : './plugins';
+var hooks = [
+ 'connect',
+ 'pre-connection',
+ 'connect',
+ 'ehlo_parse',
+ 'ehlo',
+ 'helo_parse',
+ 'helo',
+ 'auth_parse',
+ 'auth',
+ 'auth-plain',
+ 'auth-login',
+ 'auth-cram-md5',
+ 'rcpt_parse',
+ 'rcpt_pre',
+ 'rcpt',
+ 'mail_parse',
+ 'mail',
+ 'mail_pre',
+ 'data',
+ 'data_headers_end',
+ 'data_post',
+ 'queue_pre',
+ 'queue',
+ 'queue_post',
+ 'vrfy',
+ 'noop',
+ 'quit',
+ 'reset_transaction',
+ 'disconnect',
+ 'post-connection',
+ 'unrecognized_command',
+ 'deny',
+ 'ok',
+ 'received_line',
+ 'help'
+];
+
+function _load_plugin(self) {
+ self.the_plugin = require(self.full_path);
+ self.the_plugin.register.call(self);
+}
+
+function Plugin(name) {
+ var full_path = path.resolve(plugin_path, name);
+
+ this.full_path = full_path;
+ this.name = name;
+
+ this.hooks = {};
+
+ _load_plugin(this);
+}
+
+Plugin.prototype.register_hook = function(hook_name, method_name) {
+ this.hooks[hook_name] = this.hooks[hook_name] || [];
+ this.hooks[hook_name].push(method_name);
+
+ logger.log("registered hook " + hook_name + " to " + this.name + "." + method_name);
+}
+
+var plugins = exports;
+
+plugins.load_plugins = function () {
+ logger.log("Loading plugins");
+ var plugin_list = config.get('plugins');
+
+ plugins.plugin_list = plugin_list.map(plugins.load_plugin);
+};
+
+plugins.load_plugin = function(plugin) {
+ logger.log("Loading plugin: " + plugin);
+
+ // load the plugin here
+ return new Plugin(plugin);
+}
+
+plugins.load_plugins();
+
+plugins.run_hooks = function (hook, connection, params) {
+ if (!params) params = [];
+
+ logger.log("running " + hook + " hooks");
+
+ connection.hooks_to_run = [];
+
+ for (i = 0; i < plugins.plugin_list.length; i++) {
+ var plugin = plugins.plugin_list[i];
+
+ if (plugin.hooks[hook]) {
+ var j;
+ plugin.connection = connection;
+ for (j = 0; j < plugin.hooks[hook].length; j++) {
+ var hook_code_name = plugin.hooks[hook][j];
+ logger.log("adding " + hook_code_name + " to run list");
+ connection.hooks_to_run.push([plugin, hook_code_name]);
+ // plugin.the_plugin[ hook_code_name ](callback, params);
+ }
+ }
+ }
+
+ plugins.run_next_hook(hook, connection, params);
+};
+
+plugins.run_next_hook = function(hook, connection, params) {
+ var called_once = 0;
+ var callback = function(retval, msg) {
+ if (called_once) {
+ logger.log("callback called multiple times. Ignoring subsequent calls");
+ return;
+ }
+ called_once++;
+ if (!retval) retval = constants.cont;
+ if (connection.hooks_to_run.length == 0 ||
+ retval !== constants.cont)
+ {
+ var respond_method = hook + "_respond";
+ connection[respond_method](retval, msg);
+ }
+ else {
+ plugins.run_next_hook(hook, connection, params);
+ }
+ }
+
+ if (!connection.hooks_to_run.length) return callback();
+
+ // shift the next one off the stack and run it.
+ var item = connection.hooks_to_run.shift();
+ item[0].the_plugin[ item[1] ].call(item[0], callback, params);
+};
+
View
42 plugins/dnsbl.js
@@ -0,0 +1,42 @@
+// dnsbl plugin
+
+var logger = require('../logger');
+var constants = require('../constants');
+var config = require('../config');
+var dns = require('dns');
+
+exports.register = function() {
+ this.zones = config.get('dnsbl.zones');
+ this.register_hook('connect', 'check_ip');
+}
+
+exports.check_ip = function(callback) {
+ logger.log("check_ip: " + this.connection.remote_ip);
+
+ var ip = new String(this.connection.remote_ip);
+ var reverse_ip = ip.split('.').reverse().join('.');
+
+ if (!this.zones || !this.zones.length) {
+ logger.log("No zones");
+ return callback(constants.declined);
+ }
+
+ var remaining_zones = [];
+
+ this.zones.forEach(function(zone) {
+ dns.resolve(reverse_ip + "." + zone, "TXT", function (err, value) {
+ remaining_zones.pop(); // we don't care about order really
+ if (err) {
+ logger.log("DNS error: " + err);
+ if (remaining_zones.length === 0) {
+ // only call declined if no more results are pending
+ return callback(constants.declined);
+ }
+ }
+ return callback(constants.deny, value);
+ });
+
+ remaining_zones.push(zone);
+ });
+
+}
View
13 plugins/relay_all.js
@@ -0,0 +1,13 @@
+var logger = require('../logger');
+var constants = require('../constants');
+
+exports.register = function() {
+ this.register_hook('rcpt', 'confirm_all');
+};
+
+exports.confirm_all = function(callback, params) {
+ console.log(params);
+ var recipient = params.shift();
+ logger.log("confirming recipient " + recipient);
+ callback(constants.ok);
+};
View
23 plugins/test_queue.js
@@ -0,0 +1,23 @@
+
+var logger = require('../logger');
+var constants = require('../constants');
+var fs = require('fs');
+
+exports.register = function() {
+ this.register_hook('queue', 'queue_mail');
+};
+
+exports.queue_mail = function(callback) {
+ var lines = this.connection.transaction.data_lines;
+ if (lines.length === 0) {
+ return callback(constants.deny);
+ }
+
+ fs.writeFile('/tmp/mail.eml', lines.join(''), function(err) {
+ if (err) {
+ return callback(constants.deny, "Saving failed");
+ }
+
+ return callback(constants.ok);
+ });
+};
View
82 rfc1869.js
@@ -0,0 +1,82 @@
+// RFC 1869 email address parser
+
+// 6. MAIL FROM and RCPT TO Parameters
+// [...]
+//
+// esmtp-cmd ::= inner-esmtp-cmd [SP esmtp-parameters] CR LF
+// esmtp-parameters ::= esmtp-parameter *(SP esmtp-parameter)
+// esmtp-parameter ::= esmtp-keyword ["=" esmtp-value]
+// esmtp-keyword ::= (ALPHA / DIGIT) *(ALPHA / DIGIT / "-")
+//
+// ; syntax and values depend on esmtp-keyword
+// esmtp-value ::= 1*<any CHAR excluding "=", SP, and all
+// control characters (US ASCII 0-31
+// inclusive)>
+//
+// ; The following commands are extended to
+// ; accept extended parameters.
+// inner-esmtp-cmd ::= ("MAIL FROM:" reverse-path) /
+// ("RCPT TO:" forward-path)
+
+var chew_regexp = /\s+([A-Za-z0-9][A-Za-z0-9\-]*(=[^= \x00-\x1f]+)?)$/;
+
+exports.parse = function(type, line) {
+ var params = [];
+ line = (new String(line)).replace(/\s*$/, '');
+ if (type === "mail") {
+ line = line.replace(/from:/i, "");
+ }
+ else {
+ line = line.replace(/to:/i, "");
+ }
+
+ var matches;
+ while (matches = chew_regexp.exec(line)) {
+ params.push(matches[1]);
+ line = line.splice(matches[1].length);
+ }
+ params = params.reverse();
+
+ // the above will "fail" (i.e. all of the line in params) on
+ // some addresses without <> like
+ // MAIL FROM: user=name@example.net
+ // or RCPT TO: postmaster
+
+ // let's see if $line contains nothing and use the first value as address:
+ if (line.length) {
+ // parameter syntax error, i.e. not all of the arguments were
+ // stripped by the while() loop:
+ if (line.match(/\@.*\s/)) {
+ throw "Syntax error in parameters";
+ }
+
+ params.unshift(line);
+
+ return params;
+ }
+
+ line = params.shift();
+ if (type === "mail") {
+ if (!line.length) {
+ return ["<>"]; // 'MAIL FROM:' --> 'MAIL FROM:<>'
+ }
+ if (line.match(/\@.*\s/)) {
+ throw "Syntax error in parameters";
+ }
+ }
+ else {
+ if (line.match(/\@.*\s/)) {
+ throw "Syntax error in parameters";
+ }
+ else {
+ if (line.match(/\s/))
+ throw "Syntax error in parameters";
+ if (!line.match(/^(postmaster|abuse)$/i))
+ throw "Syntax error in address";
+ }
+ }
+
+ params.unshift(line);
+
+ return params;
+}
View
46 server.js
@@ -0,0 +1,46 @@
+// smtp network server
+
+var util = require('util');
+var net = require('net');
+var logger = require('./logger');
+var config = require('./config');
+var conn = require('./connection');
+
+var Server = exports;
+
+var defaults = {
+ port: 25,
+ listen_host: '0.0.0.0',
+ inactivity_timeout: 600
+};
+
+function apply_defaults(obj) {
+ var key;
+ for (key in defaults) {
+ obj[key] = obj[key] || defaults[key];
+ }
+}
+
+Server.createServer = function (params) {
+ var config_data = config.get('smtp', 'ini');
+ var param_key;
+ for (param_key in params) {
+ if (typeof params[param_key] !== 'function') {
+ config_data.main[param_key] = params[param_key];
+ }
+ }
+
+ // config_data defaults
+ apply_defaults(config_data.main);
+
+ var server = net.createServer();
+ server.on('connection', function(client) {
+ client.setTimeout(config_data.main.inactivity_time * 1000);
+ conn.createConnection(client);
+ });
+ server.listen(config_data.main.port, config_data.main.listen_host,
+ function () {
+ logger.log("Listening on port " + config_data.main.port);
+ }
+ );
+};
View
32 transaction.js
@@ -0,0 +1,32 @@
+// An SMTP Transaction
+
+var config = require('./config');
+var logger = require('./logger');
+
+var trans = exports;
+
+function Transaction() {
+ this.mail_from = null;
+ this.rcpt_to = [];
+ this.helo_host = null;
+ this.data_lines = [];
+ this.notes = {};
+}
+
+exports.Transaction = Transaction;
+
+exports.createTransaction = function() {
+ var t = new Transaction();
+ return t;
+};
+
+Transaction.prototype.mail_from = function() {
+ if (arguments.length) {
+ this.mail_from = arguments[0];
+ }
+ return this.mail_from;
+};
+
+Transaction.prototype.data_add = function(line) {
+ this.data_lines.push(line);
+};

0 comments on commit 7b3a080

Please sign in to comment.
Something went wrong with that request. Please try again.