mx-connect
Version:
Establish TCP connection to a MX server with MTA-STS and DANE/TLSA support
161 lines (141 loc) • 6.06 kB
JavaScript
/**
* @fileoverview Resolves MX hostnames to IP addresses.
* Performs parallel A and AAAA record lookups for all MX entries,
* then filters out invalid/local addresses based on configuration.
* @module resolve-ip
*/
;
const net = require('net');
const dnsErrors = require('./dns-errors');
const tools = require('./tools');
/**
* Resolves a specific DNS record type for an MX hostname.
*
* Returns an empty array for "not found" errors (ENODATA, ENOTFOUND),
* allowing the connection to proceed with other record types.
* For actual DNS failures, embeds the error in the result array
* to be handled during filtering.
*
* @param {Function} dnsResolve - Promise-based DNS resolver
* @param {string} exchange - MX hostname to resolve
* @param {string|null} type - Record type ('AAAA') or null for A records
* @returns {Promise<Array>} Array of IP addresses or [{error, exchange}] on failure
* @private
*/
function resolveRecordType(dnsResolve, exchange, type) {
const resolvePromise = type ? dnsResolve(exchange, type) : dnsResolve(exchange);
return resolvePromise.then(
list => list || [],
err => {
// "Not found" is not an error - just means no records of this type
if (tools.isNotFoundError(err)) {
return [];
}
// Embed error in array for later handling during filtering
return [{ error: err, exchange }];
}
);
}
/**
* Resolves IP addresses for all MX entries in parallel.
*
* For each MX hostname, performs A record lookup (IPv4) and optionally
* AAAA record lookup (IPv6 unless ignoreIPv6=true). All lookups run
* concurrently via Promise.all for performance.
*
* After resolution, filters out invalid addresses (local, private, broadcast)
* based on configuration. Throws if no valid addresses remain.
*
* @param {Object} delivery - Delivery object with MX entries to resolve
* @param {Array} delivery.mx - Array of MX entries with exchange hostnames
* @param {Object} [delivery.dnsOptions] - DNS configuration options
* @param {boolean} [delivery.dnsOptions.ignoreIPv6=false] - Skip AAAA lookups
* @param {Function} [delivery.dnsOptions.resolve] - Custom DNS resolver
* @returns {Promise<Object>} Delivery object with populated A/AAAA arrays
* @throws {Error} If no valid IP addresses can be resolved (error.category = 'dns')
*/
async function resolveIP(delivery) {
// Track first error and whether any valid address was found
let firstError = null;
let addressFound = false;
/**
* Filters IP addresses and embedded errors from resolution results.
* Captures the first error encountered for later if all addresses fail.
* @private
*/
const filterAddress = ip => {
// Handle DNS resolution errors embedded in the array by resolveRecordType
if (ip && ip.error) {
if (!firstError) {
const err = ip.error;
err.exchange = ip.exchange;
err.response = `DNS Error: Unable to resolve the IP address for the specified host [${ip.exchange}] of the Mail Exchange (MX) server for the domain "${delivery.domain}". ${dnsErrors[err.code] || err.message}`;
err.category = 'dns';
err.temporary = true;
firstError = err;
}
return false;
}
// Check if IP is invalid (local, private, broadcast, etc.)
const invalid = tools.isInvalid(delivery, ip);
if (invalid) {
if (!firstError) {
const detail = typeof invalid === 'string' ? `. ${invalid}` : '';
firstError = new Error(
`Unable to deliver email to the IP address [${ip}] resolved for the Mail Exchange (MX) server of "${delivery.domain}"${detail}`
);
firstError.code = 'InvalidIpAddress';
firstError.response = `DNS Error: ${firstError.message}`;
firstError.category = 'dns';
}
return false;
}
addressFound = true;
return true;
};
const dnsOptions = delivery.dnsOptions || {};
const dnsResolve = tools.getDnsResolver(dnsOptions.resolve);
// Build array of resolution promises using flatMap for parallel execution
// Each MX entry generates 1-2 promises (A and optionally AAAA)
const resolutionPromises = delivery.mx.flatMap(entry => {
if (!entry.exchange) {
return [];
}
// Skip DNS lookup if exchange is already an IP address
if (net.isIP(entry.exchange)) {
if (net.isIPv4(entry.exchange)) {
entry.A = [entry.exchange];
} else if (net.isIPv6(entry.exchange)) {
entry.AAAA = [entry.exchange];
}
return [];
}
// Always resolve A records (IPv4)
const tasks = [resolveRecordType(dnsResolve, entry.exchange, null).then(list => (entry.A = list))];
// Optionally resolve AAAA records (IPv6)
if (!dnsOptions.ignoreIPv6) {
tasks.push(resolveRecordType(dnsResolve, entry.exchange, 'AAAA').then(list => (entry.AAAA = list)));
}
return tasks;
});
// Execute all DNS lookups in parallel for performance
await Promise.all(resolutionPromises);
// Filter out invalid/local addresses from all entries
for (const entry of delivery.mx) {
entry.A = entry.A.filter(filterAddress);
entry.AAAA = entry.AAAA.filter(filterAddress);
}
// Throw error if no valid addresses remain
if (!addressFound) {
if (firstError) {
throw firstError;
}
const error = new Error(`Failed to resolve any IP addresses for the Mail Exchange (MX) server associated with "${delivery.domain}"`);
error.code = 'ENOTFOUND';
error.response = `DNS Error: ${error.message}`;
error.category = 'dns';
throw error;
}
return delivery;
}
module.exports = resolveIP;