haraka/haraka-plugin-recipient-routes

View on GitHub
index.js

Summary

Maintainability
A
2 hrs
Test Coverage
// rcpt_to.routes - per email/domain mail routes
//
// validates incoming recipients against flat file & Redis
// routes mail based on per-email or per-domain specified routes

const urlparser = require('url');

exports.register = function () {
  this.inherits('haraka-plugin-redis');

  this.cfg = {};
  this.route_list={};

  this.load_rcpt_to_routes_ini();
  if (this.cfg.redis.opts.enabled) {
    this.merge_redis_ini();

    this.register_hook('init_master',  'init_redis_plugin');
    this.register_hook('init_child',   'init_redis_plugin');
  }

  this.register_hook('rcpt',   'rcpt');
  this.register_hook('get_mx', 'get_mx');
}

exports.load_rcpt_to_routes_ini = function () {
  const plugin = this;
  plugin.cfg = plugin.config.get('rcpt_to.routes.ini', {
    booleans: [
      '+redis.enabled',
    ],
  },
  function () {
    plugin.load_rcpt_to_routes_ini();
  })

  if (!plugin.cfg.redis) plugin.cfg.redis = {};

  plugin.cfg.redis.opts = {
    host: plugin.cfg.redis.server_ip || plugin.cfg.redis.host || '127.0.0.1',
    port: plugin.cfg.redis.server_port || plugin.cfg.redis.port || 6379,
    enabled: plugin.cfg.redis.enabled ?? true,
  }

  const lowered = {};
  if (plugin.cfg.routes) {
    const keys = Object.keys(plugin.cfg.routes);
    for (const key of keys) {
      lowered[key.toLowerCase()] = plugin.cfg.routes[key];
    }
    plugin.route_list = lowered;
  }
}

exports.do_file_search = async function (txn, address, domain) {

  if (this.route_list[address]) {
    txn.results.add(this, { pass: 'file.email' });
    return OK
  }

  if (this.route_list[domain])  {
    txn.results.add(this, { pass: 'file.domain' });
    return OK
  }

  // not permitted (by this rcpt_to plugin)
  txn.results.add(this, { fail: 'file' });
}

exports.get_rcpt_address = function (rcpt) {

  if (!rcpt.host) return [ rcpt.address().toLowerCase() ];

  return [ rcpt.address().toLowerCase(), rcpt.host.toLowerCase() ];
}

exports.do_redis_search = async function (connection, address, domain) {

  const replies = await this.db.multi()
    .get(address)
    .get(domain)
    .exec()

  try {
    // any replies from Redis with an MX?
    if (replies[0]) {
      connection.transaction.results.add(this, {pass: 'redis.email'});
      return OK;
    }
    if (replies[1]) {
      connection.transaction.results.add(this, {pass: 'redis.domain'});
      return OK
    }

    // no redis record, try files
    return await this.do_file_search(connection.transaction, address, domain);
  }
  catch (err) {
    connection.results.add(this, { err });
  }
}

exports.rcpt = async function (next, connection, params) {

  const txn = connection.transaction;
  if (!txn) return next();

  const [address, domain] = this.get_rcpt_address(params[0]);
  if (!domain) {      // ignore RCPT TO without an @
    txn.results.add(this, {fail: 'rcpt!domain'});
    return next();
  }

  // if we can't use redis, try files
  if (!this.cfg.redis.opts.enabled || !this.db || !await this.redis_ping()) {
    return next(await this.do_file_search(txn, address, domain));
  }

  // redis connection open, try it
  next(await this.do_redis_search(connection, address, domain))
}

exports.parse_mx = function (entry) {

  const uri = new urlparser.parse(entry);

  if ( uri.protocol == 'lmtp:' ) {
    return {
      exchange: uri.hostname,
      port: uri.port,
      using_lmtp: true,
    }
  }

  if ( uri.protocol == 'smtp:' ) {
    return {
      exchange: uri.hostname,
      port: uri.port,
    }
  }

  return entry;
}

exports.get_mx_file = function (address, domain, next) {

  // check email adress for route
  if (this.route_list[address]) {
    return next(OK, this.parse_mx(this.route_list[address]));
  }

  // check email domain for route
  if (this.route_list[domain]) {
    return next(OK, this.parse_mx(this.route_list[domain]));
  }

  this.loginfo(`using DNS MX for: ${address}`);
  next();
}

exports.get_mx = async function (next, hmail, domain) {

  // get email address
  let address = domain.toLowerCase();
  if (hmail && hmail.todo && hmail.todo.rcpt_to && hmail.todo.rcpt_to[0]) {
    address = hmail.todo.rcpt_to[0].address().toLowerCase();
  }
  else {
    this.logerror('no rcpt from hmail, using domain' );
  }

  // if we can't use redis, try files and return
  if (!this.cfg.redis.opts.enabled || !this.db || !await this.redis_ping()) {
    this.get_mx_file(address, domain, next);
    return;
  }

  // redis connection open, try it
  const replies = await this.db.multi()
    .get(address)
    .get(domain)
    .exec()

  try {
    // got replies from Redis, any with an MX?
    if (replies[0]) return next(OK, this.parse_mx(replies[0]));
    if (replies[1]) return next(OK, this.parse_mx(replies[1]));

    // no redis record, try files
    this.get_mx_file(address, domain, next);
  }
  catch (err) {
    this.logerror(err);
    next();
  }
}

exports.insert_route = function (email, route) {
  // for importing, see http://redis.io/topics/mass-insert
  if (!this.cfg.redis.opts.enabled || !this.db || !this.redis_pings) return false;

  this.db.set(email, route);
}

exports.delete_route = function (email) {
  if (!this.cfg.redis.opts.enabled || !this.redis_pings) return false;

  this.db.del(email);
}