Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

A plugin that supports aliases. Please feel free to extend aliases

functionality.  This plugin is also implemented with complete tests.
  • Loading branch information...
commit 2204fc57bfebd2c15ee3b9b9eec09efa3a9fcba9 1 parent 0e053b8
@godsflaw godsflaw authored
View
7 address.js
@@ -1,7 +1,12 @@
"use strict";
// a class encapsulating an email address as per RFC-2821
-var logger = require('./logger');
+// Since we don't need logger in production, we comment it out so that
+// our test don't have to stub out the Address object, but can instead
+// use the real thing. If debugging is needed in this module, simply
+// add your logs and uncomment this require. Just make sure to comment
+// out again when everything is working. If you don't tests will hang.
+// var logger = require('./logger');
var qchar = /([^a-zA-Z0-9!#\$\%\&\x27\*\+\x2D\/=\?\^_`{\|}~.])/;
View
2  config/aliases
@@ -0,0 +1,2 @@
+{
+}
View
128 docs/plugins/aliases.md
@@ -0,0 +1,128 @@
+aliases
+=======
+
+This plugin allows one to configure aliases that may perform an action or
+change the RCPT address in a number of ways. All aliases are specified in
+a JSON formatted configuration file, and must have at very least an action.
+Any syntax error found in the JSON format config file will stop the server
+from running.
+
+WARNING: DO NOT USE THIS PLUGIN WITH queue/smtp_proxy.
+
+Configuration
+-------------
+
+* aliases
+
+ JSON formatted configuration file that must contain, at very least, a key
+ to match against RCPT address, and a value that is an associative array
+ with an "action" : "<action>" key, value pair. An example:
+
+ { "test1" : { "action" : "drop" } }
+
+ In the above example the "test1" alias will drop any message that matches
+ test1, or test1-* (wildcard '-', see below). Actions may in turn have 0 or
+ more options listed with them like so:
+
+ { "test3" : { "action" : "alias", "to" : "test3-works" } }
+
+ In the above example the "test3" alias has an action of "alias", and
+ a required "to" field. If this "to" field were missing the alias would
+ fail to run, and an error would be printed in the logs.
+
+ * wildcard '-' notation
+
+ In an effort to match some of the functionality of other alias parsers
+ we've allowed wildcard matching of the alias against the right most
+ string of a RCPT address. That is, if our address were
+ test2-testing@example.com, the below alias would match:
+
+ { "test2" : { "action" : "drop" } }
+
+ The larger, and more specific alias, should always match first when
+ using wildcard '-' notation. So if the above RCPT were put up against
+ this alias config, it would not drop, but rather map to another
+ address:
+
+ {
+ "test2" : { "action" : "drop" },
+ "test2-testing" : { "action" : "alias", "to" : "test@foo.com" }
+ }
+
+ * chaining and circuits
+
+ In short, we do not allow chaining of aliases at this time. As a
+ side-effect, we enjoy protections against alias circuits.
+
+ * optional one line formatting
+
+ Any valid JSON will due, however, please consider keeping each alias
+ on its own line so that others that wish to grep the aliases file
+ have an easier time finding the full configuration for an alias.
+
+ * nondeterministic duplicate matches
+
+ This plugin was written with speed in mind. That means every lookup
+ hashes into the alias file for its match. While the act of doing so
+ is fast, it does mean that any duplicate alias entries will match
+ nondeterministically. That is, we cannot predict what will happen
+ here:
+
+ {
+ "coinflip" : { "action" : "alias", "to" : "heads@coin.com" },
+ "coinflip" : { "action" : "alias", "to" : "tails@coin.com" }
+ }
+
+ Truth be told, one result will likely always be chosen over the other,
+ so this is not exactly a coinflip. We simply cannot say what the
+ language implementation will do here, it could change tomorrow.
+
+* action (required)
+
+ The following is a list of supported actions, and the options they require.
+
+ * drop
+
+ This action simply drops a message, while pretending everything was
+ okay to the sender. This acts much like an alias to /dev/null in
+ other servers.
+
+ * alias
+
+ This action will map the alias key to the address specified in the
+ "to" option. A note about matching in addition to the note
+ about wildcard '-' above. When we match an alias, we store the
+ hostname of the match for a shortcut substitution syntax later.
+
+ * to (required)
+
+ This option is the full address, or local part at matched hostname
+ that the RCPT address will be re-written to. For an example of
+ an alias to a full address consider the following:
+
+ { "test5" : { "action" : "alias", "to" : "test5@foo.com" } }
+
+ This will map RCPT matches for "test5" to "test5-works@foo.com".
+ This would map "test5@somedomain.com" to "test5-works@foo.com"
+ every time. Now compare this notation with its shortcut
+ counterpart, best used when the "to" address is at the same
+ domain as the match:
+
+ { "test4" : { "action" : "alias", "to" : "test4" } }
+
+ Clearly, this notation is more compact, but what does it do. Well,
+ mail to "test4-foo@anydomain.com" will map to "test4@anydomain.com".
+ One can see the clear benefit of using this notation with lots of
+ aliases on a single domain that map to other local parts at the
+ same domain.
+
+Example Configuration
+---------------------
+{
+ "test1" : { "action" : "drop" },
+ "test2" : { "action" : "drop" },
+ "test3" : { "action" : "alias", "to" : "test3-works" },
+ "test4" : { "action" : "alias", "to" : "test4" },
+ "test5" : { "action" : "alias", "to" : "test5-works@success.com" },
+ "test6" : { "action" : "alias", "to" : "test6-works@success.com" },
+}
View
68 plugins/aliases.js
@@ -0,0 +1,68 @@
+// This is the aliases plugin
+// One must not run this plugin with the queue/smtp_proxy plugin.
+
+exports.register = function () {
+ this.inherits('queue/discard');
+
+ this.register_hook('rcpt','aliases');
+};
+
+exports.aliases = function (next, connection, params) {
+ var plugin = this;
+ var config = this.config.get('aliases', 'json') || {};
+ var rcpt = params[0].address();
+ var user = params[0].user;
+ var host = params[0].host;
+ var match = user.split("-", 1);
+ var action = "<missing>";
+
+ if (config[user] || config[match[0]]) {
+ if (config[user]) {
+ action = config[user].action || action;
+ match = user;
+ }
+ else {
+ action = config[match[0]].action || action;
+ match = match[0];
+ }
+
+ switch (action.toLowerCase()) {
+ case 'drop':
+ _drop(plugin, connection, rcpt);
+ break;
+ case 'alias':
+ _alias(plugin, connection, match, config[match], host);
+ break;
+ default:
+ connection.loginfo(plugin, "unknown action: " + action);
+ }
+ }
+
+ next();
+};
+
+function _drop(plugin, connection, rcpt) {
+ connection.logdebug(plugin, "marking " + rcpt + " for drop");
+ connection.transaction.notes.discard = true;
+}
+
+function _alias(plugin, connection, key, config, host) {
+ var to;
+
+ if (config.to) {
+ if (config.to.search("@") !== -1) {
+ to = config.to;
+ }
+ else {
+ to = config.to + '@' + host;
+ }
+
+ connection.logdebug(plugin, "aliasing " +
+ connection.transaction.rcpt_to + " to " + to);
+ connection.transaction.rcpt_to = to;
+ }
+ else {
+ connection.loginfo(plugin, 'alias failed for ' + key +
+ ', no "to" field in alias config');
+ }
+}
View
1  tests/fixtures/stub_plugin.js
@@ -5,6 +5,7 @@ var stub = require('tests/fixtures/stub');
var plugin = exports;
function Plugin(name) {
+ this.inherits = stub();
}
plugin.createPlugin = function(name) {
View
276 tests/plugins/aliases.js
@@ -0,0 +1,276 @@
+var stub = require('tests/fixtures/stub'),
+ constants = require('../../constants'),
+ Address = require('../../address').Address,
+ Connection = require('tests/fixtures/stub_connection'),
+ Plugin = require('tests/fixtures/stub_plugin');
+
+// huge hack here, but plugin tests need constants
+constants.import(global);
+
+function _set_up(callback) {
+ this.backup = {};
+
+ // needed for tests
+ this.plugin = Plugin.createPlugin('plugins/aliases');
+ this.connection = Connection.createConnection();
+ this.recip = new Address('<test1@example.com>');
+ this.params = [this.recip];
+ // backup modifications
+
+ this.backup.plugin = {};
+ this.backup.plugin.register_hook = this.plugin.register_hook;
+
+ // stub out functions
+ this.plugin.config = stub();
+ this.plugin.register_hook = stub();
+ this.connection.loginfo = stub();
+ this.connection.logdebug = stub();
+ this.connection.notes = stub();
+ this.connection.transaction = stub();
+ this.connection.transaction.notes = stub();
+
+ // some test data
+ this.configfile = {
+ "test1" : { "action" : "drop" },
+ "test2" : { "action" : "drop" },
+ "test2-specific" : { "action" : "alias", "to" : "test2" },
+ "test3" : { "action" : "alias", "to" : "test3-works" },
+ "test4" : { "action" : "alias", "to" : "test4" },
+ "test5" : { "action" : "alias", "to" : "test5-works@success.com" },
+ "test6" : { "action" : "alias", "to" : "test6-works@success.com" },
+ "test7" : { "action" : "fail", "to" : "should_fail" },
+ "test8" : { "to" : "should_fail" },
+ "test9" : { "action" : "alias" }
+ };
+ this.plugin.config.get = function (file, type) {
+ return this.configfile;
+ }.bind(this);
+
+ // going to need these in multiple tests
+ this.plugin.register();
+
+ callback();
+}
+
+function _tear_down(callback) {
+ // restore backed up functions
+ this.plugin.register_hook = this.backup.plugin.register_hook;
+
+ callback();
+}
+
+exports.aliases = {
+ setUp : _set_up,
+ tearDown : _tear_down,
+ 'should have register function' : function (test) {
+ test.expect(2);
+ test.isNotNull(this.plugin);
+ test.isFunction(this.plugin.register);
+ test.done();
+ },
+ 'register function should inherit from queue/discard' : function (test) {
+ test.expect(2);
+ test.ok(this.plugin.inherits.called);
+ test.equals(this.plugin.inherits.args[0], 'queue/discard');
+ test.done();
+ },
+ 'register function should call register_hook()' : function (test) {
+ test.expect(1);
+ test.ok(this.plugin.register_hook.called);
+ test.done();
+ },
+ 'register_hook() should register for propper hook' : function (test) {
+ test.expect(1);
+ test.equals(this.plugin.register_hook.args[0], 'rcpt');
+ test.done();
+ },
+ 'register_hook() should register available function' : function (test) {
+ test.expect(3);
+ test.equals(this.plugin.register_hook.args[1], 'aliases');
+ test.isNotNull(this.plugin.aliases);
+ test.isFunction(this.plugin.aliases);
+ test.done();
+ },
+ 'aliases hook always returns next()' : function (test) {
+ var next = function (action) {
+ test.expect(1);
+ test.isUndefined(action);
+ test.done();
+ };
+
+ this.plugin.aliases(next, this.connection, this.params);
+ },
+ 'should drop test1@example.com' : function (test) {
+ var next = function (action) {
+ test.expect(1);
+ test.ok(this.connection.transaction.notes.discard);
+ test.done();
+ }.bind(this);
+
+ this.plugin.aliases(next, this.connection, this.params);
+ },
+ 'should drop test2-testing@example.com' : function (test) {
+ // these will get reset in _set_up everytime
+ this.recip = new Address('<test2-testing@example.com>');
+ this.params = [this.recip];
+
+ var next = function (action) {
+ test.expect(1);
+ test.ok(this.connection.transaction.notes.discard);
+ test.done();
+ }.bind(this);
+
+ this.plugin.aliases(next, this.connection, this.params);
+ },
+ 'should drop test2-specific@example.com' : function (test) {
+ // these will get reset in _set_up everytime
+ this.recip = new Address('<test2-specific@example.com>');
+ this.params = [this.recip];
+
+ var next = function (action) {
+ test.expect(3);
+ test.isUndefined(this.connection.transaction.notes.discard);
+ test.isNotNull(this.connection.transaction.rcpt_to);
+ test.equals(this.connection.transaction.rcpt_to,
+ "test2@example.com");
+ test.done();
+ }.bind(this);
+
+ this.plugin.aliases(next, this.connection, this.params);
+ },
+ 'should map test3@example.com to test3-works@example.com' : function (test) {
+ // these will get reset in _set_up everytime
+ this.recip = new Address('<test3@example.com>');
+ this.params = [this.recip];
+
+ var next = function (action) {
+ test.expect(2);
+ test.isNotNull(this.connection.transaction.rcpt_to);
+ test.equals(this.connection.transaction.rcpt_to,
+ "test3-works@example.com");
+ test.done();
+ }.bind(this);
+
+ this.plugin.aliases(next, this.connection, this.params);
+ },
+ 'should map test4-testing@example.com to test4@example.com' : function (test) {
+ // these will get reset in _set_up everytime
+ this.recip = new Address('<test4-testing@example.com>');
+ this.params = [this.recip];
+
+ var next = function (action) {
+ test.expect(2);
+ test.isNotNull(this.connection.transaction.rcpt_to);
+ test.equals(this.connection.transaction.rcpt_to,
+ "test4@example.com");
+ test.done();
+ }.bind(this);
+
+ this.plugin.aliases(next, this.connection, this.params);
+ },
+ 'should map test5@example.com to test5-works@success.com' : function (test) {
+ // these will get reset in _set_up everytime
+ this.recip = new Address('<test5@example.com>');
+ this.params = [this.recip];
+
+ var next = function (action) {
+ test.expect(2);
+ test.isNotNull(this.connection.transaction.rcpt_to);
+ test.equals(this.connection.transaction.rcpt_to,
+ "test5-works@success.com");
+ test.done();
+ }.bind(this);
+
+ this.plugin.aliases(next, this.connection, this.params);
+ },
+ 'should map test6-testing@example.com to test6-works@success.com' : function (test) {
+ // these will get reset in _set_up everytime
+ this.recip = new Address('<test6-testing@example.com>');
+ this.params = [this.recip];
+
+ var next = function (action) {
+ test.expect(2);
+ test.isNotNull(this.connection.transaction.rcpt_to);
+ test.equals(this.connection.transaction.rcpt_to,
+ "test6-works@success.com");
+ test.done();
+ }.bind(this);
+
+ this.plugin.aliases(next, this.connection, this.params);
+ },
+ 'should not drop test1@example.com, no config' : function (test) {
+ // empty config data
+ this.configfile = {};
+ this.plugin.config.get = function (file, type) {
+ return this.configfile;
+ }.bind(this);
+
+ var next = function (action) {
+ test.expect(1);
+ test.isUndefined(this.connection.transaction.notes.discard);
+ test.done();
+ }.bind(this);
+
+ this.plugin.aliases(next, this.connection, this.params);
+ },
+ 'should not drop test1@example.com, config undefined' : function (test) {
+ // undefined config data
+ this.configfile = undefined;
+ this.plugin.config.get = function (file, type) {
+ return this.configfile;
+ }.bind(this);
+
+ var next = function (action) {
+ test.expect(1);
+ test.isUndefined(this.connection.transaction.notes.discard);
+ test.done();
+ }.bind(this);
+
+ this.plugin.aliases(next, this.connection, this.params);
+ },
+ 'should fail with loginfo on unknown action' : function (test) {
+ // these will get reset in _set_up everytime
+ this.recip = new Address('<test7@example.com>');
+ this.params = [this.recip];
+
+ var next = function (action) {
+ test.expect(2);
+ test.ok(this.connection.loginfo.called);
+ test.equals(this.connection.loginfo.args[1],
+ "unknown action: " + this.configfile["test7"].action);
+ test.done();
+ }.bind(this);
+
+ this.plugin.aliases(next, this.connection, this.params);
+ },
+ 'should fail with loginfo on missing action' : function (test) {
+ // these will get reset in _set_up everytime
+ this.recip = new Address('<test8@example.com>');
+ this.params = [this.recip];
+
+ var next = function (action) {
+ test.expect(2);
+ test.ok(this.connection.loginfo.called);
+ test.equals(this.connection.loginfo.args[1],
+ "unknown action: <missing>");
+ test.done();
+ }.bind(this);
+
+ this.plugin.aliases(next, this.connection, this.params);
+ },
+ 'action alias should fail with loginfo on missing to' : function (test) {
+ // these will get reset in _set_up everytime
+ this.recip = new Address('<test9@example.com>');
+ this.params = [this.recip];
+
+ var next = function (action) {
+ test.expect(2);
+ test.ok(this.connection.loginfo.called);
+ test.equals(this.connection.loginfo.args[1],
+ 'alias failed for test9, no "to" field in alias config');
+ test.done();
+ }.bind(this);
+
+ this.plugin.aliases(next, this.connection, this.params);
+ }
+};
Please sign in to comment.
Something went wrong with that request. Please try again.