Permalink
Browse files

Improve mail_from.is_resolvable plugin

  • Loading branch information...
1 parent 9c38bf3 commit e1371dc79d79ab43d53376cecbb25d31e7a98a1a @smfreegard smfreegard committed Nov 10, 2011
Showing with 132 additions and 43 deletions.
  1. +3 −3 config/mail_from.is_resolvable.ini
  2. +14 −17 docs/plugins/mail_from.is_resolvable.md
  3. +115 −23 plugins/mail_from.is_resolvable.js
@@ -1,3 +1,3 @@
-[general]
-timeout=60
-timeout_msg=timed out when looking up sender's MX.
+timeout=30
+allow_mx_ip=0
+reject_no_mx=1
@@ -4,27 +4,24 @@ mail_from.is_resolvable
This plugin checks that the domain used in MAIL FROM is resolvable to an MX
record.
+Configuration
+-------------
-Configuration mail_from.is_resolvable.ini
-------------------------------------------
+This plugin uses the INI-style file format and accepts the following options:
-This is the general configuration file for the plugin.
+* timeout
-* mail_from.is_resolvable.general.timeout
+ Default: 30
+ Maximum limit in seconds for queries to complete. If the timeout is
+ reached a TEMPFAIL is returned to the client.
- How long we should give this plugin before we time it out (seconds).
+* allow_mx_ip=[0|1]
+ Allow MX records that return IP addresses instead of hostnames.
+ This is not allowed as per the RFC, but some MTAs allow it.
-* mail_from.is_resolvable.general.timeout_msg
+* reject_no_mx=[0|1]
- Text to send when plugin reaches timeout (text).
-
-
-Configuration mail_from.is_resolvable.timeout
----------------------------------------------
-
-This is how we specify to Haraka that our plugin should have a certain timeout.
-If you specify 0 here, then the plugin will never timeout while the connection
-is active. This is also required for this plugin, which needs to handle its
-own timeouts. To actually specify the timeout for this plugin, please see
-the general config in mail_from.is_resolvable.ini.
+ Return DENY and reject the command if no MX record is found. Otherwise a
+ DENYSOFT (TEMPFAIL) is returned and the client will retry later.
+ DNS errors always return DENYSOFT, so this should be safe to enable.
@@ -1,6 +1,6 @@
// Check MAIL FROM domain is resolvable to an MX
-
var dns = require('dns');
+var re_bogus_ip = /^(?:0\.0\.0\.0|255\.255\.255\.255|127\.)/;
exports.hook_mail = function(next, connection, params) {
var mail_from = params[0];
@@ -11,38 +11,130 @@ exports.hook_mail = function(next, connection, params) {
}
var called_next = 0;
- var timeout_id = 0;
var plugin = this;
var domain = mail_from.host;
var config = this.config.get('mail_from.is_resolvable.ini');
- var timeout = config.general && (config.general['timeout'] || 60);
- var timeout_msg = config.general && (config.general['timeout_msg'] || '');
// Just in case DNS never comes back (UDP), we should DENYSOFT.
- timeout_id = setTimeout(function () {
- plugin.loginfo('timed out when looking up ' + domain +
- '\'s MX record. Disconnecting.');
+ var timeout_id = setTimeout(function () {
+ plugin.loginfo('timed out when looking up MX for ' + domain);
called_next++;
- return next(DENYSOFT, timeout_msg);
- }, timeout * 1000);
+ return next(DENYSOFT, 'Temporary resolver error (timeout)');
+ }, ((config.main.timeout) ? config.main.timeout : 30) * 1000);
- dns.resolveMx(domain, function(err, addresses) {
- if (called_next) {
- // This happens when we've called next() from our plugin timeout
- // handler, but we eventually get a response from DNS. We do not
- // want to call next() again, so we just return.
- return;
- }
- if (err && err.code != dns.NXDOMAIN && err.code != 'ENOTFOUND') {
- plugin.logerror("DNS Error: " + err);
+ var cb = function (code, reply) {
+ if (!called_next) {
clearTimeout(timeout_id);
- return next(DENYSOFT, "Temporary resolver error");
+ called_next++;
+ next(code, reply);
+ }
+ }
+
+ dns.resolveMx(domain, function(err, addresses) {
+ if (err) {
+ plugin.logdebug(domain + ': MX => ' + err.message);
+ switch (err.code) {
+ case dns.NXDOMAIN:
+ case 'ENOTFOUND':
+ case 'ENODATA':
+ // Ignore these as they are not 'temporary'
+ // In this case we need to look up the implicit MX
+ break;
+ default:
+ return cb(DENYSOFT, 'Temporary resolver error ('
+ + err.code + ')');
+ break;
+ }
}
if (addresses && addresses.length) {
- clearTimeout(timeout_id);
- return next();
+ // Verify that the MX records resolve to valid addresses
+ var a_records = {};
+ var pending_queries = 0;
+ var check_results = function () {
+ a_records = Object.keys(a_records);
+ if (a_records && a_records.length) {
+ plugin.logdebug(domain + ': ' + a_records);
+ return cb();
+ }
+ return cb(((config.main.reject_no_mx) ? DENY : DENYSOFT),
+ 'No MX for your FROM address');
+ }
+
+ addresses.forEach(function (addr) {
+ // Handle MX records that are IP addresses
+ // This is invalid - but a lot of MTAs allow it.
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(addr.exchange)) {
+ connection.logwarn(domain + ': invalid MX ' + addr.exchange)
+ if (config.main.allow_mx_ip) {
+ a_records[addr.exchange] = 1;
+ }
+ return;
+ }
+ pending_queries++;
+ dns.resolve(addr.exchange, function(err, addresses) {
+ pending_queries--;
+ if (err) {
+ plugin.logdebug(domain + ': MX ' + addr.priority + ' '
+ + addr.exchange + ' => ' + err.message);
+ }
+ else {
+ plugin.logdebug(domain + ': MX ' + addr.priority + ' '
+ + addr.exchange + ' => ' + addresses);
+ for (var i=0; i < addresses.length; i++) {
+ // Ignore anything obviously bogus
+ if (re_bogus_ip.test(addresses[i])) {
+ plugin.logdebug(addr.exchange + ': discarding ' + addresses[i]);
+ continue;
+ }
+ a_records[addresses[i]] = 1;
+ }
+ }
+ if (pending_queries === 0) {
+ check_results();
+ }
+ });
+ });
+ // In case we don't run any queries
+ if (pending_queries === 0) {
+ check_results();
+ }
+ }
+ else {
+ // Check for implicit MX 0 record
+ dns.resolve(domain, function(err, addresses) {
+ if (err) {
+ plugin.logdebug(domain + ': A => ' + err.message);
+ switch (err.code) {
+ case dns.NXDOMAIN:
+ case 'ENOTFOUND':
+ case 'ENODATA':
+ // Ignore
+ break;
+ default:
+ return cb(DENYSOFT, 'Temporary resolver error ('
+ + err.code + ')');
+ break;
+ }
+ }
+ if (addresses && addresses.length) {
+ plugin.logdebug(domain + ': A => ' + addresses);
+ var a_records = {};
+ for (var i=0; i < addresses.length; i++) {
+ // Ignore anything obviously bogus
+ if (re_bogus_ip.test(addresses[i])) {
+ plugin.logdebug(domain + ': discarding ' + addresses[i]);
+ continue;
+ }
+ a_records[addresses[i]] = 1;
+ }
+ a_records = Object.keys(a_records);
+ if (a_records && a_records.length) {
+ return cb();
+ }
+ }
+ return cb(((config.main.reject_no_mx) ? DENY : DENYSOFT),
+ 'No MX for your FROM address');
+ });
}
- clearTimeout(timeout_id);
- return next(DENYSOFT, "No MX for your FROM address");
});
}

0 comments on commit e1371dc

Please sign in to comment.