diff --git a/.release b/.release index 36bb27a..0fa4e69 160000 --- a/.release +++ b/.release @@ -1 +1 @@ -Subproject commit 36bb27a93862517943e04f24fd67b0df2da6cbbe +Subproject commit 0fa4e690ffabb0157e46d56f18e4f7cfe49ce291 diff --git a/CHANGELOG.md b/CHANGELOG.md index 761abc0..b96fe48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). ### Unreleased +### [1.7.0] - 2024-04-29 + +- feat: added HarakaMx #89 +- test: added get_implicit_mx tests #89 +- change: get_mx: don't filter implicit MX errors #89 +- fix(get_public_ip): set timeout in stun request, fixes #84 + ### [1.6.0] - 2024-04-17 - feat: normalizeDomain, for punycode/IDN names -- feat: get_mx now _also_ returns implicit MX records +- feat: get*mx now \_also* returns implicit MX records - feat: added get_implicit_mx - feat: added resolve_mx_hosts - doc(Changes): fixed broken tag version links @@ -62,88 +69,88 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). - chore(ci): populate test matrix with Node.js LTS versions - chore(ci): limit dependabot updates to production deps -#### [1.3.5] - 2022-05-27 +### [1.3.5] - 2022-05-27 - chore(ci): use shared GHA workflows - style(es6): use dns.promises internally - dep(async): replace async dependency with Promise.all - doc(README): use code fences around examples (vs indention) -#### [1.3.4] - 2022-01-05 +### [1.3.4] - 2022-01-05 - promisify get_ips_by_host (backwards compatible) -#### [1.3.3] - 2020-01-05 +### [1.3.3] - 2020-01-05 - refactored is_local_host function to return a promise instead of using a callback #65 -#### [1.3.2] - 2021-12-20 +### [1.3.2] - 2021-12-20 - add is_local_host function #63 -#### [1.3.1] - 2021-10-13 +### [1.3.1] - 2021-10-13 - get_mx: wrap dns.resolveMx in a try haraka/Haraka#2985 - add .release scripts - add GH workflow, publish release to NPM upon merge to master -#### 1.3.0 - 2021-01-23 +### 1.3.0 - 2021-01-23 - Support passing an array to ip_in_list #60 -#### 1.2.4 - 2021-01-14 +### 1.2.4 - 2021-01-14 - add "any" IP to is_local_ip - add TEST-NET-[1-3] to is_private_ip -#### 1.2.3 - 2020-12-19 +### 1.2.3 - 2020-12-19 - fix: restore the tests wrapping the resolveMX iterable -#### 1.2.2 - 2020-12-15 +### 1.2.2 - 2020-12-15 - get_mx: do not include implicit MX -#### [1.2.1] - 2020-11-17 +### [1.2.1] - 2020-11-17 - bump ipaddr.js to 2.0.0 #56 -#### [1.2.0] - 2020-06-23 +### [1.2.0] - 2020-06-23 - added get_mx - remove deprecated load_tls_ini - remove deprecated tls_ini_section_with_defaults -#### 1.1.5 - 2020-04-11 +### 1.1.5 - 2020-04-11 - ipv6_bogus: handle parsing broken ipv6 addresses #49 - update async to version 3.0.1 #43 -#### 1.1.4 - 2019-04-04 +### 1.1.4 - 2019-04-04 - stop is_private_ip from checking if the IP is bound to a local network interface -#### 1.1.3 - 2019-03-01 +### 1.1.3 - 2019-03-01 - is_local_ip checks local network interfaces too -#### 1.1.2 - 2018-11-03 +### 1.1.2 - 2018-11-03 - add is_local_ip -#### 1.1.1 - 2018-07-19 +### 1.1.1 - 2018-07-19 - ip_in_list doesn't throw on empty list -#### 1.1.0 - 2018-04-11 +### 1.1.0 - 2018-04-11 - add get_primary_host_name haraka/Haraka#2380 -#### 1.0.14 - 2018-01-25 +### 1.0.14 - 2018-01-25 - restore tls_ini_section_with_defaults function (deprecated since Haraka 2.0.17) -#### 1.0.13 - 2018-01-19 +### 1.0.13 - 2018-01-19 - get_public_ip: assign timer before calling connect #29 - avoid race where timeout isn't cleared because stun connect errors immediately @@ -152,40 +159,40 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). - eslint updates #25, #27 - improved x509 parser #22 -#### 1.0.10 - 2017-07-27 +### 1.0.10 - 2017-07-27 - added vs-stun as optional dep (from Haraka) #21 -#### 1.0.9 - 2017-06-16 +### 1.0.9 - 2017-06-16 - lint fixes for compat with eslint 4 #18 -#### 1.0.8 - 2017-03-08 +### 1.0.8 - 2017-03-08 - skip loading expired x509 (TLS) certs - make TLS cert dir configurable - rename certs -> cert (be consistent with haraka/plugins/tls) - store cert/key as buffers (was strings) -#### 1.0.7 - 2017-03-08 +### 1.0.7 - 2017-03-08 - handle undefined tls.ini section -#### 1.0.6 - 2017-03-04 +### 1.0.6 - 2017-03-04 - add tls_ini_section_with_defaults() - add load_tls_dir() - add parse_x509_names() -#### 1.0.5 - 2016-11-20 +### 1.0.5 - 2016-11-20 - add enableSNI TLS option -#### 1.0.4 - 2016-10-25 +### 1.0.4 - 2016-10-25 - initialize TLS opts in (section != main) as booleans -#### 1.0.3 - 2016-10-25 +### 1.0.3 - 2016-10-25 - added tls.ini loading @@ -197,7 +204,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). [1.3.4]: https://github.com/haraka/haraka-net-utils/releases/tag/1.3.4 [1.3.5]: https://github.com/haraka/haraka-net-utils/releases/tag/1.3.5 [1.3.6]: https://github.com/haraka/haraka-net-utils/releases/tag/1.3.6 -[1.3.7]: https://github.com/haraka/haraka-net-utils/releases/tag/v1.3.7 +[1.3.7]: https://github.com/haraka/haraka-net-utils/releases/tag/1.3.7 [1.4.0]: https://github.com/haraka/haraka-net-utils/releases/tag/v1.4.0 [1.4.1]: https://github.com/haraka/haraka-net-utils/releases/tag/v1.4.1 [1.5.0]: https://github.com/haraka/haraka-net-utils/releases/tag/v1.5.0 @@ -206,3 +213,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). [1.5.3]: https://github.com/haraka/haraka-net-utils/releases/tag/v1.5.3 [1.5.4]: https://github.com/haraka/haraka-net-utils/releases/tag/v1.5.4 [1.6.0]: https://github.com/haraka/haraka-net-utils/releases/tag/v1.6.0 +[1.7.0]: https://github.com/haraka/haraka-net-utils/releases/tag/v1.7.0 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index ac187dd..90ae255 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -2,7 +2,7 @@ This handcrafted artisinal software is brought to you by: -|
msimerson (57) |
baudehlo (4) |
DoobleD (2) |
lnedry (2) |
Juerd (1) |
olsonpm (1) |
typingArtist (1) | +|
msimerson (58) |
baudehlo (4) |
DoobleD (2) |
lnedry (2) |
Juerd (1) |
olsonpm (1) |
typingArtist (1) | | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | this file is maintained by [.release](https://github.com/msimerson/.release) diff --git a/README.md b/README.md index 96ee3d8..6d1f69d 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,49 @@ try { } ``` +### HarakaMx + +An object class representing a MX. HarakaMx objects may contain the following properties: + +```js +{ + exchange: '', // required: a FQDN or IP address + priority: 0, // integer, a MX priority. + port: 25, // integer: an alternate port + bind: '', // an outbound IP address to bind to + bind_helo: '', // an outbound helo hostname + using_lmtp: false, // boolean, specify LMTP delivery + auth_user: '', // an AUTH username (required if AUTH is desired) + auth_pass: '', // an AUTH password (required if AUTH is desired) + auth_type: '', // an AUTH type that should be used with the MX. + from_dns: '', // the DNS name from which the MX was queried +} +``` + +Create a HarakaMx object in The Usual Way: + +```js +const nu = require('haraka-net-utils') +const myMx = new nu.HarakaMx(parameter) +``` + +The parameter can be one of: + +- A string in any of the following formats: + - hostname + - hostname:port + - IPv4 + - IPv4:port + - [IPv6] + - [IPv6]: port +- A [URL](https://nodejs.org/docs/latest-v20.x/api/url.html) string + - smtp://mail.example.com:25 + - lmtp://int-mail.example.com:24 + - smtp://user:pass@host.example.com:587 +- An object, containing at least an exchange, and any of the other properties listed at the top of this section. + +An optional second parameter is an alias for from_dns. + [ci-img]: https://github.com/haraka/haraka-net-utils/actions/workflows/ci.yml/badge.svg [ci-url]: https://github.com/haraka/haraka-net-utils/actions/workflows/ci.yml [cov-img]: https://codecov.io/github/haraka/haraka-net-utils/coverage.svg diff --git a/index.js b/index.js index 77955e7..252e483 100644 --- a/index.js +++ b/index.js @@ -1,14 +1,14 @@ 'use strict' -// node.js built-ins -const { Resolver } = require('dns').promises +const { Resolver } = require('node:dns').promises const dns = new Resolver({ timeout: 25000, tries: 1 }) -const net = require('net') -const os = require('os') -const punycode = require('punycode.js') +const net = require('node:net') +const os = require('node:os') +const url = require('node:url') // npm modules const ipaddr = require('ipaddr.js') +const punycode = require('punycode.js') const sprintf = require('sprintf-js').sprintf const tlds = require('haraka-tld') @@ -287,7 +287,9 @@ exports.get_public_ip_async = async function () { }, timeout * 1000) // Connect to STUN Server - const res = await this.stun.request(get_stun_server()) + const res = await this.stun.request(get_stun_server(), { + maxTimeout: (timeout - 1) * 1000, + }) this.public_ip = res.getXorAddress().address clearTimeout(timer) return this.public_ip @@ -296,22 +298,21 @@ exports.get_public_ip_async = async function () { exports.get_public_ip = async function (cb) { if (!cb) return exports.get_public_ip_async() - const nu = this - if (nu.public_ip !== undefined) return cb(null, nu.public_ip) // cache + if (this.public_ip !== undefined) return cb(null, this.public_ip) // cache // manual config override, for the cases where we can't figure it out const smtpIni = exports.config.get('smtp.ini').main if (smtpIni.public_ip) { - nu.public_ip = smtpIni.public_ip - return cb(null, nu.public_ip) + this.public_ip = smtpIni.public_ip + return cb(null, this.public_ip) } // Initialise cache value to null to prevent running // should we hit a timeout or the module isn't installed. - nu.public_ip = null + this.public_ip = null try { - nu.stun = require('@msimerson/stun') + this.stun = require('@msimerson/stun') } catch (e) { e.install = 'Please install stun: "npm install -g stun"' console.error(`${e.msg}\n${e.install}`) @@ -324,17 +325,20 @@ exports.get_public_ip = async function (cb) { }, timeout * 1000) // Connect to STUN Server - nu.stun.request(get_stun_server(), (error, res) => { - if (timer) clearTimeout(timer) - if (error) return cb(error) - - nu.public_ip = res.getXorAddress().address - cb(null, nu.public_ip) - }) + this.stun.request( + get_stun_server(), + { maxTimeout: (timeout - 1) * 1000 }, + (error, res) => { + if (timer) clearTimeout(timer) + if (error) return cb(error) + + this.public_ip = res.getXorAddress().address + cb(null, this.public_ip) + }, + ) } function get_stun_server() { - // STUN servers by Google const servers = [ 'stun.l.google.com:19302', 'stun1.l.google.com:19302', @@ -345,11 +349,7 @@ function get_stun_server() { return servers[Math.floor(Math.random() * servers.length)] } -exports.get_ipany_re = function (prefix, suffix, modifier) { - if (prefix === undefined) prefix = '' - if (suffix === undefined) suffix = '' - if (modifier === undefined) modifier = 'mg' - /* eslint-disable prefer-template */ +exports.get_ipany_re = function (prefix = '', suffix = '', modifier = 'mg') { return new RegExp( prefix + `(` + // capture group @@ -493,31 +493,24 @@ exports.get_mx = async (raw_domain, cb) => { const domain = normalizeDomain(raw_domain) try { - const exchanges = await dns.resolveMx(domain) + let exchanges = await dns.resolveMx(domain) if (exchanges && exchanges.length) { - exchanges.map((e) => (e.from_dns = domain)) + exchanges = exchanges.map((e) => new HarakaMx(e, domain)) if (cb) return cb(null, exchanges) return exchanges } + // no MX record(s), fall through } catch (err) { - // console.error(err.message) if (fatal_mx_err(err)) { if (cb) return cb(err, []) throw err } + // non-terminal DNS failure, fall through } - // no MX or non-fatal DNS failure - try { - const exchanges = await this.get_implicit_mx(domain) - if (cb) return cb(null, exchanges) - return exchanges - } catch (err) { - if (fatal_mx_err(err)) { - if (cb) return cb(err, []) - throw err - } - } + const exchanges = await this.get_implicit_mx(domain) + if (cb) return cb(null, exchanges) + return exchanges } exports.get_implicit_mx = async (domain) => { @@ -528,9 +521,7 @@ exports.get_implicit_mx = async (domain) => { return r .filter((a) => a.status === 'fulfilled') - .flatMap((a) => - a.value.map((ip) => ({ priority: 0, exchange: ip, from_dns: domain })), - ) + .flatMap((a) => a.value.map((ip) => new HarakaMx(ip, domain))) } exports.resolve_mx_hosts = async (mxes) => { @@ -570,3 +561,92 @@ exports.resolve_mx_hosts = async (mxes) => { return settled.filter((s) => s.status === 'fulfilled').flatMap((s) => s.value) } + +class HarakaMx { + constructor(obj = {}, domain) { + switch (typeof obj) { + case 'string': + /mtp:\/\//.test(obj) ? this.fromUrl(obj) : this.fromString(obj) + break + case 'object': + this.fromObject(obj) + break + } + + if (this.priority === undefined) this.priority = 0 + if (domain && this.from_dns === undefined) { + this.from_dns = domain.toLowerCase() + } + } + + fromObject(obj) { + for (const prop of [ + 'exchange', + 'priority', + 'port', + 'bind', + 'bind_helo', + 'using_lmtp', + 'auth_user', + 'auth_pass', + 'auth_type', + 'from_dns', + ]) { + if (obj[prop] !== undefined) this[prop] = obj[prop] + } + } + + fromString(str) { + const matches = /^\[?(.*?)\]?(?::(24|25|465|587|\d{4,5}))?$/.exec(str) + if (matches) { + this.exchange = matches[1].toLowerCase() + if (matches[2]) this.port = parseInt(matches[2]) + } else { + this.exchange = str + } + } + + fromUrl(str) { + const dest = new url.URL(str) + + switch (dest.protocol) { + case 'smtp:': + if (!dest.port) dest.port = 25 + break + case 'lmtp:': + this.using_lmtp = true + if (!dest.port) dest.port = 24 + break + } + + if (dest.hostname) this.exchange = dest.hostname.toLowerCase() + if (dest.port) this.port = parseInt(dest.port) + if (dest.username) this.auth_user = dest.username + if (dest.password) this.auth_pass = dest.password + } + + toUrl() { + const proto = this.using_lmtp ? 'lmtp://' : 'smtp://' + const auth = this.auth_user ? `${this.auth_user}:****@` : '' + const host = net.isIPv6(this.exchange) + ? `[${this.exchange}]` + : this.exchange + const port = this.port ? this.port : proto === 'lmtp://' ? 24 : 25 + return new url.URL(`${proto}${auth}${host}:${port}`) + } + + toString() { + const from_dns = this.from_dns ? ` (from ${this.from_dns})` : '' + return `MX ${this.priority} ${this.toUrl()}${from_dns}` + } +} + +/* + * A string of one of the following formats: + * hostname + * hostname:port + * ipaddress + * ipaddress:port + */ + +exports.HarakaMx = HarakaMx diff --git a/package.json b/package.json index dd51bb9..01b60f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "haraka-net-utils", - "version": "1.6.0", + "version": "1.7.0", "description": "haraka network utilities", "main": "index.js", "files": [ @@ -36,12 +36,12 @@ }, "homepage": "https://github.com/haraka/haraka-net-utils#readme", "devDependencies": { - "@haraka/eslint-config": "^1.1.3" + "@haraka/eslint-config": "^1.1.5" }, "dependencies": { - "haraka-config": "^1.1.0", + "haraka-config": "^1.2.4", "haraka-tld": "^1.2.1", - "ipaddr.js": "^2.1.0", + "ipaddr.js": "^2.2.0", "punycode.js": "^2.3.1", "openssl-wrapper": "^0.3.4", "sprintf-js": "^1.1.3" diff --git a/test/net_utils.js b/test/net_utils.js index a547a83..5df6153 100644 --- a/test/net_utils.js +++ b/test/net_utils.js @@ -271,11 +271,9 @@ describe('get_public_ip', function () { it('normal', function (done) { this.net_utils.public_ip = undefined const cb = function (err, ip) { - // console.log(`ip: ${ip}`); - // console.log(`err: ${err}`); if (has_stun()) { if (err) { - console.log(err) + console.error(err) } else { console.log(`stun success: ${ip}`) assert.equal(null, err) @@ -1285,7 +1283,6 @@ describe('get_mx', function () { this.net_utils.get_mx(c, (err, mxlist) => { if (err) console.error(err) assert.ifError(err) - // assert.ok(mxlist.length); checkValid(validCases[c], mxlist) done() }) @@ -1294,7 +1291,6 @@ describe('get_mx', function () { it(`awaits MX records for ${c}`, async function () { this.timeout(12000) const mxlist = await this.net_utils.get_mx(c) - // assert.ok(mxlist.length); checkValid(validCases[c], mxlist) }) } @@ -1303,6 +1299,8 @@ describe('get_mx', function () { const invalidCases = { invalid: /queryMx (ENODATA|ENOTFOUND|ESERVFAIL) invalid/, 'gmail.xn--com-0da': /(ENOTFOUND|Cannot convert name to ASCII)/, + 'non-exist.haraka.tnpi.net': /ignore/, + 'haraka.non-exist': /ignore/, } function checkInvalid(expected, actual) { @@ -1316,8 +1314,8 @@ describe('get_mx', function () { for (const c in invalidCases) { it(`cb does not crash on invalid name: ${c}`, function () { this.net_utils.get_mx(c, (err, mxlist) => { + if (err) checkInvalid(invalidCases[c], err.message) assert.equal(mxlist.length, 0) - checkInvalid(invalidCases[c], err.message) }) }) @@ -1389,3 +1387,236 @@ describe('resolve_mx_hosts', function () { assert.equal(r.length, 8) }) }) + +describe('get_implicit_mx', function () { + beforeEach(function (done) { + this.net_utils = require('../index') + done() + }) + + it('harakamail.com', async function () { + const mf = await this.net_utils.get_implicit_mx('harakamail.com') + assert.equal(mf.length, 1) + }) + + it('mx.theartfarm.com', async function () { + const mf = await this.net_utils.get_implicit_mx('mx.theartfarm.com') + assert.equal(mf.length, 0) + }) + + it('resolve-fail-definitive.josef-froehle.de', async function () { + const mf = await this.net_utils.get_implicit_mx( + 'resolve-fail-definitive.josef-froehle.de', + ) + assert.equal(mf.length, 0) + }) + it('resolve-fail-a.josef-froehle.de', async function () { + const mf = await this.net_utils.get_implicit_mx( + 'resolve-fail-a.josef-froehle.de', + ) + assert.equal(mf.length, 1) + }) + it('resolve-fail-aaaa.josef-froehle.de', async function () { + const mf = await this.net_utils.get_implicit_mx( + 'resolve-fail-aaaa.josef-froehle.de', + ) + assert.equal(mf.length, 0) + }) +}) + +describe('HarakaMx', () => { + beforeEach(function (done) { + this.nu = require('../index') + done() + }) + + describe('fromObject', () => { + it('accepts an object', function () { + assert.deepEqual( + new this.nu.HarakaMx({ + from_dns: 'example.com', + exchange: '.', + priority: 0, + }), + { from_dns: 'example.com', exchange: '.', priority: 0 }, + ) + }) + + it('sets default priority to 0', function () { + assert.deepEqual(new this.nu.HarakaMx({ exchange: '.' }), { + exchange: '.', + priority: 0, + }) + }) + + it('if optional domain provided, sets from_dns', function () { + assert.deepEqual(new this.nu.HarakaMx({ exchange: '.' }, 'example.com'), { + from_dns: 'example.com', + exchange: '.', + priority: 0, + }) + }) + }) + + describe('fromString', function () { + it('parses a hostname', function () { + assert.deepEqual(new this.nu.HarakaMx('mail.example.com'), { + exchange: 'mail.example.com', + priority: 0, + }) + }) + + it('parses a hostname:port', function () { + assert.deepEqual(new this.nu.HarakaMx('mail.example.com:25'), { + exchange: 'mail.example.com', + port: 25, + priority: 0, + }) + }) + + it('parses an IPv4', function () { + assert.deepEqual(new this.nu.HarakaMx('192.0.2.1'), { + exchange: '192.0.2.1', + priority: 0, + }) + }) + + it('parses an IPv4:port', function () { + assert.deepEqual(new this.nu.HarakaMx('192.0.2.1:25'), { + exchange: '192.0.2.1', + port: 25, + priority: 0, + }) + }) + + it('parses an IPv6', function () { + assert.deepEqual(new this.nu.HarakaMx('2001:db8::1'), { + exchange: '2001:db8::1', + priority: 0, + }) + }) + + it('parses an IPv6:port', function () { + assert.deepEqual(new this.nu.HarakaMx('2001:db8::1:25'), { + exchange: '2001:db8::1', + port: 25, + priority: 0, + }) + }) + + it('parses an [IPv6]:port', function () { + assert.deepEqual(new this.nu.HarakaMx('[2001:db8::1]:25'), { + exchange: '2001:db8::1', + port: 25, + priority: 0, + }) + }) + }) + + describe('fromUri', function () { + it('parses simple URIs', function () { + assert.deepEqual(new this.nu.HarakaMx('smtp://192.0.2.2'), { + exchange: '192.0.2.2', + port: 25, + priority: 0, + }) + + assert.deepEqual(new this.nu.HarakaMx('smtp://[2001:db8::1]:25'), { + exchange: '[2001:db8::1]', + port: 25, + priority: 0, + }) + }) + + it('parses more complex URIs', function () { + assert.deepEqual( + new this.nu.HarakaMx('smtp://authUser:sekretPass@[2001:db8::1]'), + { + exchange: '[2001:db8::1]', + port: 25, + priority: 0, + auth_pass: 'sekretPass', + auth_user: 'authUser', + }, + ) + + assert.deepEqual( + new this.nu.HarakaMx('lmtp://authUser:sekretPass@[2001:db8::1]:25'), + { + exchange: '[2001:db8::1]', + port: 25, + priority: 0, + using_lmtp: true, + auth_pass: 'sekretPass', + auth_user: 'authUser', + }, + ) + }) + }) + + describe('toUrl', function () { + it('has a reasonable toUrl()', function () { + assert.equal( + new this.nu.HarakaMx({ exchange: '.' }).toUrl(), + 'smtp://.:25', + ) + + assert.equal( + new this.nu.HarakaMx({ + from_dns: 'example.com', + exchange: '.', + priority: 10, + }).toUrl(), + 'smtp://.:25', + ) + + assert.equal( + new this.nu.HarakaMx('smtp://au:ap@192.0.2.3:25').toUrl(), + 'smtp://au:****@192.0.2.3:25', + ) + + assert.equal( + new this.nu.HarakaMx('smtp://au:ap@192.0.2.3:465').toUrl(), + 'smtp://au:****@192.0.2.3:465', + ) + + assert.equal( + new this.nu.HarakaMx('smtp://[2001:db8::1]:25').toUrl(), + 'smtp://[2001:db8::1]:25', + ) + }) + }) + + describe('toString', function () { + it('has a reasonable toString()', function () { + assert.equal( + new this.nu.HarakaMx({ exchange: '.' }).toString(), + 'MX 0 smtp://.:25', + ) + + assert.equal( + new this.nu.HarakaMx({ + from_dns: 'example.com', + exchange: '.', + priority: 10, + }).toString(), + 'MX 10 smtp://.:25 (from example.com)', + ) + + assert.equal( + new this.nu.HarakaMx('smtp://au:ap@192.0.2.3:25').toString(), + 'MX 0 smtp://au:****@192.0.2.3:25', + ) + + assert.equal( + new this.nu.HarakaMx('smtp://au:ap@192.0.2.3:465').toString(), + 'MX 0 smtp://au:****@192.0.2.3:465', + ) + + assert.equal( + new this.nu.HarakaMx('smtp://[2001:db8::1]:25').toString(), + 'MX 0 smtp://[2001:db8::1]:25', + ) + }) + }) +})