From 1112fccb944b5ee73b051e0a8ccd824aebf2169b Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Wed, 17 Apr 2024 19:56:22 -0700 Subject: [PATCH] Release v1.2.5 (#22) - get_mx: filter out implicit MX records - dep: eslint-plugin-haraka -> @haraka/eslint-config - chore: lint: remove duplicate / stale rules from .eslintrc - chore: populate [files] in package.json. Delete .npmignore. - doc(CONTRIBUTORS): added - doc(CHANGES): renamed CHANGELOG - chore: prettier & lint --- .codeclimate.yml | 8 +- .eslintrc.yaml | 22 +- .github/PULL_REQUEST_TEMPLATE.md | 6 +- .github/dependabot.yml | 6 +- .github/workflows/ci.yml | 9 +- .github/workflows/codeql.yml | 6 +- .github/workflows/publish.yml | 2 +- .npmignore | 58 --- .prettierrc.yml | 2 + .release | 2 +- Changes.md => CHANGELOG.md | 32 +- CONTRIBUTORS.md | 8 + README.md | 19 +- bin/spf | 71 ++-- index.js | 388 +++++++++++-------- lib/spf.js | 645 ++++++++++++++++--------------- package.json | 29 +- test/index.js | 413 +++++++++++++------- test/spf.js | 88 +++-- 19 files changed, 978 insertions(+), 836 deletions(-) delete mode 100644 .npmignore create mode 100644 .prettierrc.yml rename Changes.md => CHANGELOG.md (66%) create mode 100644 CONTRIBUTORS.md diff --git a/.codeclimate.yml b/.codeclimate.yml index 0e443ca..c889eb8 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,10 +1,10 @@ engines: eslint: enabled: true - channel: "eslint-8" + channel: 'eslint-8' config: - config: ".eslintrc.yaml" + config: '.eslintrc.yaml' ratings: - paths: - - "**.js" + paths: + - '**.js' diff --git a/.eslintrc.yaml b/.eslintrc.yaml index fe947ea..035a400 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -2,24 +2,6 @@ env: node: true es6: true mocha: true - es2020: true + es2022: true -plugins: - - haraka - -extends: - - eslint:recommended - - plugin:haraka/recommended - -rules: - indent: [2, 2, {"SwitchCase": 1}] - -root: true - -globals: - OK: true - CONT: true - DENY: true - DENYSOFT: true - DENYDISCONNECT: true - DENYSOFTDISCONNECT: true +extends: ['@haraka'] diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 957ccf3..4221f1c 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,10 +1,12 @@ Fixes # Changes proposed in this pull request: -- -- + +- +- Checklist: + - [ ] docs updated - [ ] tests updated - [ ] Changes.md updated diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 0449e4a..d450132 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -2,9 +2,9 @@ version: 2 updates: - - package-ecosystem: "npm" - directory: "/" + - package-ecosystem: 'npm' + directory: '/' schedule: - interval: "weekly" + interval: 'weekly' allow: - dependency-type: production diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a519f2..3d01042 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,11 @@ name: CI -on: [ push, pull_request ] +on: [push, pull_request] env: CI: true jobs: - lint: uses: haraka/.github/.github/workflows/lint.yml@master @@ -15,9 +14,9 @@ jobs: # secrets: inherit ubuntu: - needs: [ lint ] + needs: [lint] uses: haraka/.github/.github/workflows/ubuntu.yml@master windows: - needs: [ lint ] - uses: haraka/.github/.github/workflows/windows.yml@master \ No newline at end of file + needs: [lint] + uses: haraka/.github/.github/workflows/windows.yml@master diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 383aca2..816e8c3 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,10 +1,10 @@ -name: "CodeQL" +name: 'CodeQL' on: push: - branches: [ master ] + branches: [master] pull_request: - branches: [ master ] + branches: [master] schedule: - cron: '18 7 * * 4' diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d489fbd..e81c15f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,4 +13,4 @@ env: jobs: publish: uses: haraka/.github/.github/workflows/publish.yml@master - secrets: inherit \ No newline at end of file + secrets: inherit diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 3e8e260..0000000 --- a/.npmignore +++ /dev/null @@ -1,58 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* - -# Runtime data -pids -*.pid -*.seed - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules -jspm_packages - -# Optional npm cache directory -.npm - -# Optional REPL history -.node_repl_history - -package-lock.json -bower_components -# Optional npm cache directory -.npmrc -.idea -.DS_Store -haraka-update.sh - -.github -.release -.codeclimate.yml -.editorconfig -.gitignore -.gitmodules -.lgtm.yml -appveyor.yml -codecov.yml -.travis.yml -.eslintrc.yaml -.eslintrc.json diff --git a/.prettierrc.yml b/.prettierrc.yml new file mode 100644 index 0000000..8ded5e0 --- /dev/null +++ b/.prettierrc.yml @@ -0,0 +1,2 @@ +singleQuote: true +semi: false diff --git a/.release b/.release index 0890e94..36bb27a 160000 --- a/.release +++ b/.release @@ -1 +1 @@ -Subproject commit 0890e945e4e061c96c7b2ab45017525904c17728 +Subproject commit 36bb27a93862517943e04f24fd67b0df2da6cbbe diff --git a/Changes.md b/CHANGELOG.md similarity index 66% rename from Changes.md rename to CHANGELOG.md index e7bd1e6..3b5c403 100644 --- a/Changes.md +++ b/CHANGELOG.md @@ -1,24 +1,33 @@ +# Changelog + +The format is based on [Keep a Changelog](https://keepachangelog.com/). ### Unreleased +### [1.2.5] - 2024-04-17 + +- get_mx: filter out implicit MX records +- dep: eslint-plugin-haraka -> @haraka/eslint-config +- chore: lint: remove duplicate / stale rules from .eslintrc +- chore: populate [files] in package.json. Delete .npmignore. +- doc(CONTRIBUTORS): added +- doc(CHANGES): renamed CHANGELOG +- chore: prettier ### [1.2.4] - 2024-02-07 - doc(README): add ini code fences, improve docs - dep(net-utils): bumped 1.5.0 -> 1.5.3 - ### [1.2.3] - 2023-07-14 - fix: Handle DNS TXT array result (#15) - ### [1.2.2] - 2023-06-22 - fix: check for DNS results befor iterating, fixes #13 - es6(lib/spf): replace `self` with `this` - ### [1.2.1] - 2023-06-19 - fix: call skip_hosts via 'this' instead of exports (#11) @@ -26,23 +35,19 @@ - es6(index): replace `plugin` with `this` - deps: bump versions to latest - ### [1.2.0] - 2023-01-19 - Export SPF class (#8) - ### [1.1.3] - 2022-12-23 - fix print log (#6) - ### [1.1.2] - 2022-12-21 - dep: depend on net-utils 1.5.0 - refactor: convert loop to for...of - ### [1.1.0] - 2022-12-17 - spf: use async/await dns @@ -52,23 +57,24 @@ - index: safeguard cfg path with optional chaining, fixes #2 - dep(nopt): bump 6 -> 7 - ### [1.0.1] - 2022-07-23 - add bin/spf - move spf.js to lib/spf.js - ### 1.0.0 - 2022-07-23 - Import from Haraka - +[1.0.0]: https://github.com/haraka/haraka-plugin-spf/releases/tag/v1.0.0 [1.0.1]: https://github.com/haraka/haraka-plugin-spf/releases/tag/1.0.1 -[1.1.2]: https://github.com/haraka/haraka-plugin-spf/releases/tag/1.1.2 +[1.1.0]: https://github.com/haraka/haraka-plugin-spf/releases/tag/v1.1.0 +[1.1.2]: https://github.com/haraka/haraka-plugin-spf/releases/tag/v1.1.2 [1.1.3]: https://github.com/haraka/haraka-plugin-spf/releases/tag/1.1.3 +[1.1.4]: https://github.com/haraka/haraka-plugin-spf/releases/tag/1.1.4 [1.2.0]: https://github.com/haraka/haraka-plugin-spf/releases/tag/1.2.0 [1.2.1]: https://github.com/haraka/haraka-plugin-spf/releases/tag/1.2.1 -[1.3.0]: https://github.com/haraka/haraka-plugin-spf/releases/tag/1.3.0 +[1.2.2]: https://github.com/haraka/haraka-plugin-spf/releases/tag/1.2.2 [1.2.3]: https://github.com/haraka/haraka-plugin-spf/releases/tag/1.2.3 -[1.2.4]: https://github.com/haraka/haraka-plugin-spf/releases/tag/1.2.4 +[1.2.4]: https://github.com/haraka/haraka-plugin-spf/releases/tag/v1.2.4 +[1.2.5]: https://github.com/haraka/haraka-plugin-spf/releases/tag/v1.2.5 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 0000000..037f735 --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,8 @@ +# Contributors + +This handcrafted artisinal software is brought to you by: + +|
msimerson (13) |
gramakri (1) |
DoobleD (1) |
smfreegard (1) |
ne4t0 (1) | +| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | + +this file is maintained by [.release](https://github.com/msimerson/.release) diff --git a/README.md b/README.md index a894e41..78b8489 100644 --- a/README.md +++ b/README.md @@ -15,12 +15,11 @@ domain whilst preserving the original return-path. ## Configuration -------------- +--- This plugin uses spf.ini for configuration and the following options are available: - ```ini [relay] context=sender (default: sender) @@ -64,27 +63,27 @@ openspf_text = true ### Things to Know -* Most senders do not publish SPF records for their mail server *hostname*, +- Most senders do not publish SPF records for their mail server _hostname_, which means that the SPF HELO test rarely passes. During observation in 2014, more spam senders have valid SPF HELO than ham senders. If you expect very little from SPF HELO validation, you might still be disappointed. -* Enabling error deferrals will cause excessive delays and perhaps bounced +- Enabling error deferrals will cause excessive delays and perhaps bounced mail for senders with broken DNS. Enable this only if you are willing to delay and sometimes lose valid mail. -* Broken SPF records by valid senders are common. Keep that in mind when +- Broken SPF records by valid senders are common. Keep that in mind when considering denial of SPF error results. If you deny on error, budget time for instructing senders on how to correct their SPF records so they can email you. -* The only deny option most sites should consider is `mfrom_fail`. That will +- The only deny option most sites should consider is `mfrom_fail`. That will reject messages that explicitely fail SPF tests. SPF failures have a high correlation with spam. However, up to 10% of ham transits forwarders and/or email lists which frequently break SPF. SPF results are best used as inputs to other plugins such as DMARC, [spamassassin](http://haraka.github.io/manual/plugins/spamassassin.html), and [karma](http://haraka.github.io/manual/plugins/karma.html). -* Heed well the implications of SPF, as described in [RFC 4408](http://tools.ietf.org/html/rfc4408#section-9.3) +- Heed well the implications of SPF, as described in [RFC 4408](http://tools.ietf.org/html/rfc4408#section-9.3) ### spf.ini default settings @@ -140,9 +139,7 @@ mfrom_permerror=false openspf_text=false ``` - -Testing -------- +## Testing This plugin also provides a command-line test tool that can be used to debug SPF issues or to check results. @@ -167,8 +164,8 @@ You can add `--debug` to the option arguments to see a full trace of the SPF pro Node does not support the SPF DNS Resource Record type. Only TXT records are checked. This is a non-issue as < 1% (as of 2014) of SPF records use the SPF RR type. Due to lack of adoption, SPF has deprecated the SPF RR type. - + [ci-img]: https://github.com/haraka/haraka-plugin-spf/actions/workflows/ci.yml/badge.svg [ci-url]: https://github.com/haraka/haraka-plugin-spf/actions/workflows/ci.yml [clim-img]: https://codeclimate.com/github/haraka/haraka-plugin-spf/badges/gpa.svg diff --git a/bin/spf b/bin/spf index 109429f..7a3e24b 100755 --- a/bin/spf +++ b/bin/spf @@ -2,46 +2,55 @@ // SPF test tool -const nopt = require('nopt'); -const path = require('path'); -const base_path = path.join(__dirname, '..'); -const SPF = require(`${base_path}/lib/spf`).SPF; -const spf = new SPF(); - -const parsed = nopt({ 'debug': Boolean, 'ip': String, 'helo': String, 'domain': String }); - -function print_usage () { - console.log('Usage: spf [--debug] --ip --helo --domain '); - process.exit(1); +const nopt = require('nopt') +const path = require('path') +const base_path = path.join(__dirname, '..') +const SPF = require(`${base_path}/lib/spf`).SPF +const spf = new SPF() + +const parsed = nopt({ + debug: Boolean, + ip: String, + helo: String, + domain: String, +}) + +function print_usage() { + console.log('Usage: spf [--debug] --ip --helo --domain ') + process.exit(1) } -if (!parsed.ip || (parsed.ip && (!parsed.domain && !parsed.helo))) { - print_usage(); +if (!parsed.ip || (parsed.ip && !parsed.domain && !parsed.helo)) { + print_usage() } if (!parsed.debug) { - SPF.prototype.log_debug = function (str) {} + SPF.prototype.log_debug = function () {} } -let domain; +let domain if (parsed.domain) { - domain = /@(.+)$/.exec(parsed.domain); + domain = /@(.+)$/.exec(parsed.domain) if (domain) { - domain = domain[1]; - } - else { - domain = parsed.domain; + domain = domain[1] + } else { + domain = parsed.domain } } -spf.check_host(parsed.ip, (domain ? domain : parsed.helo)).then((result) => { - console.log([ - `ip=${parsed.ip}`, - `helo="${(parsed.helo ? parsed.helo : '')}"`, - `domain="${(domain ? domain : '')}"`, - `result=${spf.result(result)}` - ].join(' ')); -}).catch((err) => { - console.error(`Error: ${err.message}`); - process.exit(1); -}); +spf + .check_host(parsed.ip, domain ? domain : parsed.helo) + .then((result) => { + console.log( + [ + `ip=${parsed.ip}`, + `helo="${parsed.helo ? parsed.helo : ''}"`, + `domain="${domain ? domain : ''}"`, + `result=${spf.result(result)}`, + ].join(' '), + ) + }) + .catch((err) => { + console.error(`Error: ${err.message}`) + process.exit(1) + }) diff --git a/index.js b/index.js index b72d136..0edf96d 100644 --- a/index.js +++ b/index.js @@ -1,217 +1,250 @@ // spf -const SPF = require('./lib/spf').SPF; -const net_utils = require('haraka-net-utils'); -const DSN = require('haraka-dsn'); +const SPF = require('./lib/spf').SPF +const net_utils = require('haraka-net-utils') +const DSN = require('haraka-dsn') -exports.SPF = SPF; +exports.SPF = SPF exports.register = function () { - // Override logging in SPF module - SPF.prototype.log_debug = str => this.logdebug(str); + SPF.prototype.log_debug = (str) => this.logdebug(str) - this.load_spf_ini(); + this.load_spf_ini() - this.register_hook('helo', 'helo_spf'); - this.register_hook('ehlo', 'helo_spf'); + this.register_hook('helo', 'helo_spf') + this.register_hook('ehlo', 'helo_spf') } exports.load_spf_ini = function () { - this.nu = net_utils; // so tests can set public_ip - this.SPF = SPF; - - this.cfg = this.config.get('spf.ini', { - booleans: [ - '-defer.helo_temperror', - '-defer.mfrom_temperror', - - '-defer_relay.helo_temperror', - '-defer_relay.mfrom_temperror', - - '-deny.helo_none', - '-deny.helo_softfail', - '-deny.helo_fail', - '-deny.helo_permerror', - '-deny.openspf_text', - - '-deny.mfrom_none', - '-deny.mfrom_softfail', - '-deny.mfrom_fail', - '-deny.mfrom_permerror', - - '-deny_relay.helo_none', - '-deny_relay.helo_softfail', - '-deny_relay.helo_fail', - '-deny_relay.helo_permerror', - - '-deny_relay.mfrom_none', - '-deny_relay.mfrom_softfail', - '-deny_relay.mfrom_fail', - '-deny_relay.mfrom_permerror', - '-deny_relay.openspf_text', - - '-skip.relaying', - '-skip.auth', - ] - }, - () => { this.load_spf_ini(); } - ); + this.nu = net_utils // so tests can set public_ip + this.SPF = SPF + + this.cfg = this.config.get( + 'spf.ini', + { + booleans: [ + '-defer.helo_temperror', + '-defer.mfrom_temperror', + + '-defer_relay.helo_temperror', + '-defer_relay.mfrom_temperror', + + '-deny.helo_none', + '-deny.helo_softfail', + '-deny.helo_fail', + '-deny.helo_permerror', + '-deny.openspf_text', + + '-deny.mfrom_none', + '-deny.mfrom_softfail', + '-deny.mfrom_fail', + '-deny.mfrom_permerror', + + '-deny_relay.helo_none', + '-deny_relay.helo_softfail', + '-deny_relay.helo_fail', + '-deny_relay.helo_permerror', + + '-deny_relay.mfrom_none', + '-deny_relay.mfrom_softfail', + '-deny_relay.mfrom_fail', + '-deny_relay.mfrom_permerror', + '-deny_relay.openspf_text', + + '-skip.relaying', + '-skip.auth', + ], + }, + () => { + this.load_spf_ini() + }, + ) // when set, preserve legacy config settings - for (const phase of ['helo','mail']) { + for (const phase of ['helo', 'mail']) { if (this.cfg.main[`${phase}_softfail_reject`]) { - this.cfg.deny[`${phase}_softfail`] = true; + this.cfg.deny[`${phase}_softfail`] = true } if (this.cfg.main[`${phase}_fail_reject`]) { - this.cfg.deny[`${phase}_fail`] = true; + this.cfg.deny[`${phase}_fail`] = true } if (this.cfg.main[`${phase}_temperror_defer`]) { - this.cfg.defer[`${phase}_temperror`] = true; + this.cfg.defer[`${phase}_temperror`] = true } if (this.cfg.main[`${phase}_permerror_reject`]) { - this.cfg.deny[`${phase}_permerror`] = true; + this.cfg.deny[`${phase}_permerror`] = true } } if (!this.cfg.relay) { - this.cfg.relay = { context: 'sender' }; // default/legacy + this.cfg.relay = { context: 'sender' } // default/legacy } - this.cfg.lookup_timeout = this.cfg.main.lookup_timeout || this.timeout - 1; + this.cfg.lookup_timeout = this.cfg.main.lookup_timeout || this.timeout - 1 } exports.helo_spf = async function (next, connection, helo) { - const plugin = this; + const plugin = this // bypass auth'ed or relay'ing hosts if told to - const skip_reason = this.skip_hosts(connection); + const skip_reason = this.skip_hosts(connection) if (skip_reason) { - connection.results.add(plugin, {skip: `helo(${skip_reason})`}); - return next(); + connection.results.add(plugin, { skip: `helo(${skip_reason})` }) + return next() } // Bypass private IPs if (connection.remote.is_private) { - connection.results.add(plugin, {skip: 'helo(private_ip)'}); - return next(); + connection.results.add(plugin, { skip: 'helo(private_ip)' }) + return next() } // RFC 4408, 2.1: "SPF clients must be prepared for the "HELO" // identity to be malformed or an IP address literal. if (net_utils.is_ip_literal(helo)) { - connection.results.add(plugin, {skip: 'helo(ip_literal)'}); - return next(); + connection.results.add(plugin, { skip: 'helo(ip_literal)' }) + return next() } // avoid 2nd EHLO evaluation if EHLO host is identical - const results = connection.results.get(plugin); - if (results && results.domain === helo) return next(); + const results = connection.results.get(plugin) + if (results && results.domain === helo) return next() - let timeout = false; - const spf = new SPF(); + let timeout = false + const spf = new SPF() const timer = setTimeout(() => { - timeout = true; - connection.loginfo(plugin, 'timeout'); - next(); - }, plugin.cfg.lookup_timeout * 1000); + timeout = true + connection.loginfo(plugin, 'timeout') + next() + }, plugin.cfg.lookup_timeout * 1000) try { const result = await spf.check_host(connection.remote.ip, helo, null) - if (timer) clearTimeout(timer); - if (timeout) return; - const host = connection.hello.host; - plugin.log_result(connection, 'helo', host, `postmaster@${host}`, spf.result(result)); - - connection.notes.spf_helo = result; // used between hooks + if (timer) clearTimeout(timer) + if (timeout) return + const host = connection.hello.host + plugin.log_result( + connection, + 'helo', + host, + `postmaster@${host}`, + spf.result(result), + ) + + connection.notes.spf_helo = result // used between hooks connection.results.add(plugin, { scope: 'helo', result: spf.result(result), domain: host, emit: true, - }); - if (spf.result(result) === 'Pass') connection.results.add(plugin, { pass: host }); + }) + if (spf.result(result) === 'Pass') + connection.results.add(plugin, { pass: host }) + } catch (err) { + connection.logerror(plugin, err) } - catch (err) { - connection.logerror(plugin, err); - } - next(); + next() } exports.hook_mail = async function (next, connection, params) { - const plugin = this; + const plugin = this - const txn = connection?.transaction; - if (!txn) return next(); + const txn = connection?.transaction + if (!txn) return next() // bypass auth'ed or relay'ing hosts if told to - const skip_reason = this.skip_hosts(connection); + const skip_reason = this.skip_hosts(connection) if (skip_reason) { - txn.results.add(plugin, {skip: `host(${skip_reason})`}); - return next(CONT, `skipped because host(${skip_reason})`); + txn.results.add(plugin, { skip: `host(${skip_reason})` }) + return next(CONT, `skipped because host(${skip_reason})`) } // For messages from private IP space... if (connection.remote?.is_private) { - if (!connection.relaying) return next(); + if (!connection.relaying) return next() if (plugin.cfg.relay?.context !== 'myself') { - txn.results.add(plugin, {skip: 'host(private_ip)'}); - return next(CONT, 'envelope from private IP space'); + txn.results.add(plugin, { skip: 'host(private_ip)' }) + return next(CONT, 'envelope from private IP space') } } - const mfrom = params[0].address(); - const host = params[0].host; - let spf = new SPF(); - let auth_result; + const mfrom = params[0].address() + const host = params[0].host + let spf = new SPF() + let auth_result if (connection.notes?.spf_helo) { - const h_result = connection.notes.spf_helo; - const h_host = connection.hello?.host; - plugin.save_to_header(connection, spf, h_result, mfrom, h_host, 'helo'); - if (!host) { // Use results from HELO if the return-path is null - auth_result = spf.result(h_result).toLowerCase(); - connection.auth_results(`spf=${auth_result} smtp.helo=${h_host}`); - - const sender = `<> via ${h_host}`; - return plugin.return_results(next, connection, spf, 'helo', h_result, sender); + const h_result = connection.notes.spf_helo + const h_host = connection.hello?.host + plugin.save_to_header(connection, spf, h_result, mfrom, h_host, 'helo') + if (!host) { + // Use results from HELO if the return-path is null + auth_result = spf.result(h_result).toLowerCase() + connection.auth_results(`spf=${auth_result} smtp.helo=${h_host}`) + + const sender = `<> via ${h_host}` + return plugin.return_results( + next, + connection, + spf, + 'helo', + h_result, + sender, + ) } } - if (!host) return next(); // null-sender + if (!host) return next() // null-sender - let timeout = false; + let timeout = false const timer = setTimeout(() => { - timeout = true; - connection.loginfo(plugin, 'timeout'); - next(); - }, plugin.cfg.lookup_timeout * 1000); + timeout = true + connection.loginfo(plugin, 'timeout') + next() + }, plugin.cfg.lookup_timeout * 1000) - spf.helo = connection.hello?.host; + spf.helo = connection.hello?.host - function ch_cb (err, result, ip) { - if (timer) clearTimeout(timer); - if (timeout) return; + function ch_cb(err, result, ip) { + if (timer) clearTimeout(timer) + if (timeout) return if (err) { - connection.logerror(plugin, err); - return next(); + connection.logerror(plugin, err) + return next() } - plugin.log_result(connection, 'mfrom', host, mfrom, spf.result(result), (ip ? ip : connection.remote.ip)); - plugin.save_to_header(connection, spf, result, mfrom, host, 'mailfrom', (ip ? ip : connection.remote.ip)); - - auth_result = spf.result(result).toLowerCase(); - connection.auth_results(`spf=${auth_result} smtp.mailfrom=${host}`); - - txn.notes.spf_mail_result = spf.result(result); - txn.notes.spf_mail_record = spf.spf_record; + plugin.log_result( + connection, + 'mfrom', + host, + mfrom, + spf.result(result), + ip ? ip : connection.remote.ip, + ) + plugin.save_to_header( + connection, + spf, + result, + mfrom, + host, + 'mailfrom', + ip ? ip : connection.remote.ip, + ) + + auth_result = spf.result(result).toLowerCase() + connection.auth_results(`spf=${auth_result} smtp.mailfrom=${host}`) + + txn.notes.spf_mail_result = spf.result(result) + txn.notes.spf_mail_record = spf.spf_record txn.results.add(plugin, { scope: 'mfrom', result: spf.result(result), domain: host, emit: true, - }); - if (spf.result(result) === 'Pass') connection.results.add(plugin, { pass: host }); - plugin.return_results(next, connection, spf, 'mfrom', result, mfrom); + }) + if (spf.result(result) === 'Pass') + connection.results.add(plugin, { pass: host }) + plugin.return_results(next, connection, spf, 'mfrom', result, mfrom) } try { @@ -228,91 +261,104 @@ exports.hook_mail = async function (next, connection, params) { // outbound (relaying), context=myself const my_public_ip = await net_utils.get_public_ip() - let spf_result; - if (result) spf_result = spf.result(result).toLowerCase(); + let spf_result + if (result) spf_result = spf.result(result).toLowerCase() if (spf_result && spf_result !== 'pass') { if (!my_public_ip) { - return ch_cb(new Error(`failed to discover public IP`)); + return ch_cb(new Error(`failed to discover public IP`)) } - spf = new SPF(); + spf = new SPF() const r = await spf.check_host(my_public_ip, host, mfrom) - return ch_cb(null, r, my_public_ip); + return ch_cb(null, r, my_public_ip) } - ch_cb(null, result, connection.remote.ip); - } - catch (err) { + ch_cb(null, result, connection.remote.ip) + } catch (err) { ch_cb(err) } } exports.log_result = function (connection, scope, host, mfrom, result, ip) { - const show_ip=ip ? ip : connection.remote.ip; - connection.loginfo(this, `identity=${scope} ip=${show_ip} domain="${host}" mfrom=<${mfrom}> result=${result}`); + const show_ip = ip ? ip : connection.remote.ip + connection.loginfo( + this, + `identity=${scope} ip=${show_ip} domain="${host}" mfrom=<${mfrom}> result=${result}`, + ) } -exports.return_results = function (next, connection, spf, scope, result, sender) { - const msgpre = (scope === 'helo') ? `sender ${sender}` : `sender <${sender}>`; - const deny = connection.relaying ? 'deny_relay' : 'deny'; - const defer = connection.relaying ? 'defer_relay' : 'defer'; - const sender_id = (scope === 'helo') ? connection.hello_host : sender; - let text = DSN.sec_unauthorized(`http://www.openspf.org/Why?s=${scope}&id=${sender_id}&ip=${connection.remote.ip}`); +exports.return_results = function ( + next, + connection, + spf, + scope, + result, + sender, +) { + const msgpre = scope === 'helo' ? `sender ${sender}` : `sender <${sender}>` + const deny = connection.relaying ? 'deny_relay' : 'deny' + const defer = connection.relaying ? 'defer_relay' : 'defer' + const sender_id = scope === 'helo' ? connection.hello_host : sender + let text = DSN.sec_unauthorized( + `http://www.openspf.org/Why?s=${scope}&id=${sender_id}&ip=${connection.remote.ip}`, + ) switch (result) { case spf.SPF_NONE: if (this.cfg[deny][`${scope}_none`]) { - text = this.cfg[deny].openspf_text ? text : `${msgpre} SPF record not found`; - return next(DENY, text); + text = this.cfg[deny].openspf_text + ? text + : `${msgpre} SPF record not found` + return next(DENY, text) } - return next(); + return next() case spf.SPF_NEUTRAL: case spf.SPF_PASS: - return next(); + return next() case spf.SPF_SOFTFAIL: if (this.cfg[deny][`${scope}_softfail`]) { - text = this.cfg[deny].openspf_text ? text : `${msgpre} SPF SoftFail`; - return next(DENY, text); + text = this.cfg[deny].openspf_text ? text : `${msgpre} SPF SoftFail` + return next(DENY, text) } - return next(); + return next() case spf.SPF_FAIL: if (this.cfg[deny][`${scope}_fail`]) { - text = this.cfg[deny].openspf_text ? text : `${msgpre} SPF Fail`; - return next(DENY, text); + text = this.cfg[deny].openspf_text ? text : `${msgpre} SPF Fail` + return next(DENY, text) } - return next(); + return next() case spf.SPF_TEMPERROR: if (this.cfg[defer][`${scope}_temperror`]) { - return next(DENYSOFT, `${msgpre} SPF Temporary Error`); + return next(DENYSOFT, `${msgpre} SPF Temporary Error`) } - return next(); + return next() case spf.SPF_PERMERROR: if (this.cfg[deny][`${scope}_permerror`]) { - return next(DENY, `${msgpre} SPF Permanent Error`); + return next(DENY, `${msgpre} SPF Permanent Error`) } - return next(); + return next() default: // Unknown result - connection.logerror(this, `unknown result code=${result}`); - return next(); + connection.logerror(this, `unknown result code=${result}`) + return next() } } exports.save_to_header = (connection, spf, result, mfrom, host, id, ip) => { // Add a trace header - if (!connection?.transaction) return; - - const des = result === spf.SPF_PASS ? 'designates' : 'does not designate'; - const identity = `identity=${id}; client-ip=${ip ? ip : connection.remote.ip}`; - connection.transaction.add_leading_header('Received-SPF', - `${spf.result(result)} (${connection.local.host}: domain of ${host} ${des} ${connection.remote.ip} as permitted sender) receiver=${connection.local.host}; ${identity} helo=${connection.hello.host}; envelope-from=<${mfrom}>` - ); + if (!connection?.transaction) return + + const des = result === spf.SPF_PASS ? 'designates' : 'does not designate' + const identity = `identity=${id}; client-ip=${ip ? ip : connection.remote.ip}` + connection.transaction.add_leading_header( + 'Received-SPF', + `${spf.result(result)} (${connection.local.host}: domain of ${host} ${des} ${connection.remote.ip} as permitted sender) receiver=${connection.local.host}; ${identity} helo=${connection.hello.host}; envelope-from=<${mfrom}>`, + ) } exports.skip_hosts = function (connection) { - - const skip = this?.cfg?.skip; + const skip = this?.cfg?.skip if (skip) { - if (skip.relaying && connection.relaying) return 'relay'; - if (skip.auth && connection.notes.auth_user) return 'auth'; + if (skip.relaying && connection.relaying) return 'relay' + if (skip.auth && connection.notes.auth_user) return 'auth' } } diff --git a/lib/spf.js b/lib/spf.js index 5bbe0d8..508dcde 100644 --- a/lib/spf.js +++ b/lib/spf.js @@ -1,216 +1,227 @@ -'use strict'; +'use strict' // spf -const dns = require('dns').promises; -const ipaddr = require('ipaddr.js'); +const dns = require('node:dns/promises') +const net = require('node:net') +const ipaddr = require('ipaddr.js') const net_utils = require('haraka-net-utils') class SPF { - constructor (count, been_there) { + constructor(count, been_there) { // For macro expansion // This should be set before check_host() is called - this.helo = 'unknown'; - this.spf_record = ''; + this.helo = 'unknown' + this.spf_record = '' // RFC 4408 Section 10.1 // Limit the number of mechanisms/modifiers that require DNS lookups to complete. - this.count = 0; + this.count = 0 // If we have recursed we are supplied the count - if (count) this.count = count; + if (count) this.count = count // Prevent circular references, this isn't covered in the RFC - this.been_there = {}; - if (been_there) this.been_there = been_there; + this.been_there = {} + if (been_there) this.been_there = been_there // RFC 4408 Section 10.1 - this.LIMIT = 10; + this.LIMIT = 10 // Constants - this.SPF_NONE = 1; - this.SPF_PASS = 2; - this.SPF_FAIL = 3; - this.SPF_SOFTFAIL = 4; - this.SPF_NEUTRAL = 5; - this.SPF_TEMPERROR = 6; - this.SPF_PERMERROR = 7; - - this.mech_ip4 = this.mech_ip; - this.mech_ip6 = this.mech_ip; + this.SPF_NONE = 1 + this.SPF_PASS = 2 + this.SPF_FAIL = 3 + this.SPF_SOFTFAIL = 4 + this.SPF_NEUTRAL = 5 + this.SPF_TEMPERROR = 6 + this.SPF_PERMERROR = 7 + + this.mech_ip4 = this.mech_ip + this.mech_ip6 = this.mech_ip } - const_translate (value) { - const t = {}; + const_translate(value) { + const t = {} for (const k in this) { if (typeof this[k] === 'number') { - t[this[k]] = k.toUpperCase(); + t[this[k]] = k.toUpperCase() } } - if (t[value]) return t[value]; - return 'UNKNOWN'; + if (t[value]) return t[value] + return 'UNKNOWN' } - result (value) { + result(value) { switch (value) { - case this.SPF_NONE: return 'None' - case this.SPF_PASS: return 'Pass' - case this.SPF_FAIL: return 'Fail' - case this.SPF_SOFTFAIL: return 'SoftFail' - case this.SPF_NEUTRAL: return 'Neutral' - case this.SPF_TEMPERROR: return 'TempError' - case this.SPF_PERMERROR: return 'PermError' - default: return `Unknown (${value})`; + case this.SPF_NONE: + return 'None' + case this.SPF_PASS: + return 'Pass' + case this.SPF_FAIL: + return 'Fail' + case this.SPF_SOFTFAIL: + return 'SoftFail' + case this.SPF_NEUTRAL: + return 'Neutral' + case this.SPF_TEMPERROR: + return 'TempError' + case this.SPF_PERMERROR: + return 'PermError' + default: + return `Unknown (${value})` } } - return_const (qualifier) { + return_const(qualifier) { switch (qualifier) { - case '+': return this.SPF_PASS - case '-': return this.SPF_FAIL - case '~': return this.SPF_SOFTFAIL - case '?': return this.SPF_NEUTRAL - default : return this.SPF_PERMERROR + case '+': + return this.SPF_PASS + case '-': + return this.SPF_FAIL + case '~': + return this.SPF_SOFTFAIL + case '?': + return this.SPF_NEUTRAL + default: + return this.SPF_PERMERROR } } - expand_macros (str) { - const macro = /%{([slodipvh])((?:(?:\d+)?r?)?)?([-.+,/_=])?}/ig; - let match; + expand_macros(str) { + const macro = /%{([slodipvh])((?:(?:\d+)?r?)?)?([-.+,/_=])?}/gi + let match while ((match = macro.exec(str))) { // match[1] = macro-letter // match[2] = transformers // match[3] = delimiter if (!match[3]) match[3] = '.' - let strip = /(\d+)/.exec(match[2]); - if (strip) strip = strip[1]; + let strip = /(\d+)/.exec(match[2]) + if (strip) strip = strip[1] - const reverse = (((`${match[2]}`).indexOf('r')) !== -1); - let replace; - let kind; + const reverse = `${match[2]}`.indexOf('r') !== -1 + let replace + let kind switch (match[1]) { - case 's': // sender - replace = this.mail_from; - break; - case 'l': // local-part of sender - replace = (this.mail_from.split('@'))[0]; - break; - case 'o': // domain of sender - replace = (this.mail_from.split('@'))[1]; - break; - case 'd': // domain - replace = this.domain; - break; - case 'i': // IP - replace = this.ip; - break; - case 'p': // validated domain name of IP + case 's': // sender + replace = this.mail_from + break + case 'l': // local-part of sender + replace = this.mail_from.split('@')[0] + break + case 'o': // domain of sender + replace = this.mail_from.split('@')[1] + break + case 'd': // domain + replace = this.domain + break + case 'i': // IP + replace = this.ip + break + case 'p': // validated domain name of IP // NOT IMPLEMENTED - replace = 'unknown'; - break; - case 'v': // IP version + replace = 'unknown' + break + case 'v': // IP version try { - if (this.ip_ver === 'ipv4') kind = 'in-addr'; - if (this.ip_ver === 'ipv6') kind = 'ip6'; - replace = kind; - } - catch (e) {} - break; - case 'h': // EHLO/HELO domain - replace = this.helo; - break; + if (this.ip_ver === 'ipv4') kind = 'in-addr' + if (this.ip_ver === 'ipv6') kind = 'ip6' + replace = kind + } catch (e) {} + break + case 'h': // EHLO/HELO domain + replace = this.helo + break } // Process any transformers if (replace) { if (reverse || strip) { - replace = replace.split(match[3]); + replace = replace.split(match[3]) if (strip) { - strip = ((strip > replace.length) ? replace.length : strip); - replace = replace.slice(0,strip); + strip = strip > replace.length ? replace.length : strip + replace = replace.slice(0, strip) } - if (reverse) replace = replace.reverse(); - replace = replace.join('.'); + if (reverse) replace = replace.reverse() + replace = replace.join('.') } - str = str.replace(match[0], replace); + str = str.replace(match[0], replace) } } // Process any other expansions - return str - .replace(/%%/g, '%') - .replace(/%_/g, ' ') - .replace(/%-/g, '%20'); + return str.replace(/%%/g, '%').replace(/%_/g, ' ').replace(/%-/g, '%20') } - log_debug (str) { - console.error(str); + log_debug(str) { + console.error(str) } - valid_ip (ip) { - const ip_split = /^:([^/ ]+)(?:\/([^ ]+))?$/.exec(ip); + valid_ip(ip) { + const ip_split = /^:([^/ ]+)(?:\/([^ ]+))?$/.exec(ip) if (!ip_split) { - this.log_debug(`invalid IP address: ${ip}`); - return false; + this.log_debug(`invalid IP address: ${ip}`) + return false } if (!ipaddr.isValid(ip_split[1])) { - this.log_debug(`invalid IP address: ${ip_split[1]}`); - return false; + this.log_debug(`invalid IP address: ${ip_split[1]}`) + return false } - return true; + return true } - async check_host (ip, domain, mail_from) { - domain = domain.toLowerCase(); + async check_host(ip, domain, mail_from) { + domain = domain.toLowerCase() mail_from = mail_from ? mail_from.toLowerCase() : `postmaster@${domain}` - this.ipaddr = ipaddr.parse(ip); - this.ip_ver = this.ipaddr.kind(); + this.ipaddr = ipaddr.parse(ip) + this.ip_ver = this.ipaddr.kind() if (this.ip_ver === 'ipv6') { - this.ip = this.ipaddr.toString(); - } - else { - this.ip = ip; + this.ip = this.ipaddr.toString() + } else { + this.ip = ip } - this.domain = domain; - this.mail_from = mail_from; + this.domain = domain + this.mail_from = mail_from - this.log_debug(`ip=${ip} domain=${domain} mail_from=${mail_from}`); + this.log_debug(`ip=${ip} domain=${domain} mail_from=${mail_from}`) - const mech_array = []; - const mod_array = []; + const mech_array = [] + const mod_array = [] // Get the SPF record for domain let txt_rrs try { txt_rrs = await dns.resolveTxt(domain) - } - catch (err) { - this.log_debug(`error looking up TXT record: ${err.message}`); + } catch (err) { + this.log_debug(`error looking up TXT record: ${err.message}`) switch (err.code) { case dns.NOTFOUND: case dns.NODATA: - case dns.NXDOMAIN: return this.SPF_NONE - default: return this.SPF_TEMPERROR + case dns.NXDOMAIN: + return this.SPF_NONE + default: + return this.SPF_TEMPERROR } } - let spf_record; - let match; + let spf_record + let match for (let txt_rr of txt_rrs) { // txt_rr might be an array, so handle that case if (Array.isArray(txt_rr)) { - txt_rr = txt_rr.join(''); + txt_rr = txt_rr.join('') } - match = /^(v=spf1(?:$|\s.+$))/i.exec(txt_rr); + match = /^(v=spf1(?:$|\s.+$))/i.exec(txt_rr) if (!match) { - this.log_debug(`discarding TXT record: ${txt_rr}`); - continue; + this.log_debug(`discarding TXT record: ${txt_rr}`) + continue } if (!spf_record) { - this.log_debug(`found SPF record for domain ${domain}: ${match[1]}`); - spf_record = match[1].replace(/\s+/, ' ').toLowerCase(); - } - else { - this.log_debug(`found additional SPF record for domain ${domain}: ${match[1]}`); + this.log_debug(`found SPF record for domain ${domain}: ${match[1]}`) + spf_record = match[1].replace(/\s+/, ' ').toLowerCase() + } else { + this.log_debug( + `found additional SPF record for domain ${domain}: ${match[1]}`, + ) return this.SPF_PERMERROR } } @@ -218,79 +229,82 @@ class SPF { if (!spf_record) return this.SPF_NONE // No SPF record? // Store the SPF record used in the object - this.spf_record = spf_record; + this.spf_record = spf_record // Validate SPF record and build call chain - const mech_regexp1 = /^([-+~?])?(all|a|mx|ptr)$/; - const mech_regexp2 = /^([-+~?])?(a|mx|ptr|ip4|ip6|include|exists)((?::[^/ ]+(?:\/\d+(?:\/\/\d+)?)?)|\/\d+(?:\/\/\d+)?)$/; - const mod_regexp = /^([^ =]+)=([a-z0-9:/._-]+)$/; - const split = spf_record.split(' '); + const mech_regexp1 = /^([-+~?])?(all|a|mx|ptr)$/ + const mech_regexp2 = + /^([-+~?])?(a|mx|ptr|ip4|ip6|include|exists)((?::[^/ ]+(?:\/\d+(?:\/\/\d+)?)?)|\/\d+(?:\/\/\d+)?)$/ + const mod_regexp = /^([^ =]+)=([a-z0-9:/._-]+)$/ + const split = spf_record.split(' ') for (const mechanism of split) { - if (!mechanism) continue; // Skip blanks + if (!mechanism) continue // Skip blanks const obj = {} - if ((match = (mech_regexp1.exec(mechanism) || mech_regexp2.exec(mechanism)))) { + if ( + (match = mech_regexp1.exec(mechanism) || mech_regexp2.exec(mechanism)) + ) { // match: 1=qualifier, 2=mechanism, 3=optional args - if (!match[1]) match[1] = '+'; - this.log_debug(`found mechanism: ${match}`); + if (!match[1]) match[1] = '+' + this.log_debug(`found mechanism: ${match}`) if (match[2] === 'ip4' || match[2] === 'ip6') { if (!this.valid_ip(match[3])) return this.SPF_PERMERROR - } - else { + } else { // Validate macro strings if (match[3] && /%[^{%+-]/.exec(match[3])) { - this.log_debug('invalid macro string'); + this.log_debug('invalid macro string') return this.SPF_PERMERROR } if (match[3]) { // Expand macros - match[3] = this.expand_macros(match[3]); + match[3] = this.expand_macros(match[3]) } } - obj[match[2]] = [ match[1], match[3] ]; - mech_array.push(obj); + obj[match[2]] = [match[1], match[3]] + mech_array.push(obj) // console.log(mech_array) - } - else if ((match = mod_regexp.exec(mechanism))) { - this.log_debug(`found modifier: ${match}`); + } else if ((match = mod_regexp.exec(mechanism))) { + this.log_debug(`found modifier: ${match}`) // match[1] = modifier // match[2] = name // Make sure we have a method if (!this[`mod_${match[1]}`]) { - this.log_debug(`skipping unknown modifier: ${match[1]}`); - } - else { - obj[match[1]] = match[2]; - mod_array.push(obj); + this.log_debug(`skipping unknown modifier: ${match[1]}`) + } else { + obj[match[1]] = match[2] + mod_array.push(obj) // console.log(mod_array) } - } - else { + } else { // Syntax error - this.log_debug(`syntax error: ${mechanism}`); + this.log_debug(`syntax error: ${mechanism}`) return this.SPF_PERMERROR } } - this.log_debug(`SPF record for '${this.domain}' validated OK`); + this.log_debug(`SPF record for '${this.domain}' validated OK`) // Run all the mechanisms first for (const mech of mech_array) { - - const func = Object.keys(mech); - const args = mech[func]; + const func = Object.keys(mech) + const args = mech[func] // console.log(`running mechanism: ${func} args=${args} domain=${this.domain}`); - this.log_debug(`running mechanism: ${func} args=${args} domain=${this.domain}`); + this.log_debug( + `running mechanism: ${func} args=${args} domain=${this.domain}`, + ) if (this.count > this.LIMIT) { - this.log_debug('lookup limit reached'); + this.log_debug('lookup limit reached') return this.SPF_PERMERROR } - const result = await this[`mech_${func}`](((args && args.length) ? args[0] : null), ((args && args.length) ? args[1] : null)); + const result = await this[`mech_${func}`]( + args && args.length ? args[0] : null, + args && args.length ? args[1] : null, + ) // console.log(result) // If we have a result other than SPF_NONE @@ -299,15 +313,16 @@ class SPF { // run any modifiers for (const mod of mod_array) { - - const func = Object.keys(mod); - const args = mod[func]; - this.log_debug(`running modifier: ${func} args=${args} domain=${this.domain}`); - const result = await this[`mod_${func}`](args); + const func = Object.keys(mod) + const args = mod[func] + this.log_debug( + `running modifier: ${func} args=${args} domain=${this.domain}`, + ) + const result = await this[`mod_${func}`](args) // Check limits if (this.count > this.LIMIT) { - this.log_debug('lookup limit reached'); + this.log_debug('lookup limit reached') return this.SPF_PERMERROR } @@ -318,49 +333,53 @@ class SPF { return this.SPF_NEUTRAL // default if no more mechanisms } - async mech_all (qualifier, args) { + async mech_all(qualifier) { return this.return_const(qualifier) } - async mech_include (qualifier, args) { - const domain = args.substr(1); + async mech_include(qualifier, args) { + const domain = args.substr(1) // Avoid circular references if (this.been_there[domain]) { - this.log_debug(`circular reference detected: ${domain}`); + this.log_debug(`circular reference detected: ${domain}`) return this.SPF_NONE } - this.count++; - this.been_there[domain] = true; + this.count++ + this.been_there[domain] = true // Recurse - const recurse = new SPF(this.count, this.been_there); + const recurse = new SPF(this.count, this.been_there) try { const result = await recurse.check_host(this.ip, domain, this.mail_from) - this.log_debug(`mech_include: domain=${domain} returned=${this.const_translate(result)}`); + this.log_debug( + `mech_include: domain=${domain} returned=${this.const_translate(result)}`, + ) switch (result) { - case this.SPF_PASS: return this.SPF_PASS + case this.SPF_PASS: + return this.SPF_PASS case this.SPF_FAIL: case this.SPF_SOFTFAIL: - case this.SPF_NEUTRAL: return this.SPF_NONE - case this.SPF_TEMPERROR: return this.SPF_TEMPERROR - default: return this.SPF_PERMERROR + case this.SPF_NEUTRAL: + return this.SPF_NONE + case this.SPF_TEMPERROR: + return this.SPF_TEMPERROR + default: + return this.SPF_PERMERROR } - } - catch (err) { + } catch (err) { // ignore } } - async mech_exists (qualifier, args) { - this.count++; - const exists = args.substr(1); + async mech_exists(qualifier, args) { + this.count++ + const exists = args.substr(1) try { const addrs = await dns.resolve(exists) - this.log_debug(`mech_exists: ${exists} result=${addrs.join(',')}`); + this.log_debug(`mech_exists: ${exists} result=${addrs.join(',')}`) return this.return_const(qualifier) - } - catch (err) { - this.log_debug(`mech_exists: ${err}`); + } catch (err) { + this.log_debug(`mech_exists: ${err}`) switch (err.code) { case dns.NOTFOUND: case dns.NODATA: @@ -372,44 +391,44 @@ class SPF { } } - async mech_a (qualifier, args) { - this.count++; + async mech_a(qualifier, args) { + this.count++ // Parse any arguments - let cm; - let cidr4; - let cidr6; + let cm + let cidr4 + let cidr6 if (args && (cm = /\/(\d+)(?:\/\/(\d+))?$/.exec(args))) { - cidr4 = cm[1]; - cidr6 = cm[2]; + cidr4 = cm[1] + cidr6 = cm[2] } - let dm; - let domain = this.domain; + let dm + let domain = this.domain if (args && (dm = /^:([^/ ]+)/.exec(args))) { - domain = dm[1]; + domain = dm[1] } // Calculate with IP method to use - let resolve_method; - let cidr; + let resolve_method + let cidr if (this.ip_ver === 'ipv4') { - cidr = cidr4; - resolve_method = 'resolve4'; - } - else if (this.ip_ver === 'ipv6') { - cidr = cidr6; - resolve_method = 'resolve6'; + cidr = cidr4 + resolve_method = 'resolve4' + } else if (this.ip_ver === 'ipv6') { + cidr = cidr6 + resolve_method = 'resolve6' } // Use current domain let addrs try { addrs = await dns[resolve_method](domain) - } - catch (err) { - this.log_debug(`mech_a: ${err}`); + } catch (err) { + this.log_debug(`mech_a: ${err}`) switch (err.code) { case dns.NOTFOUND: case dns.NODATA: - case dns.NXDOMAIN: return this.SPF_NONE - default: return this.SPF_TEMPERROR + case dns.NXDOMAIN: + return this.SPF_NONE + default: + return this.SPF_TEMPERROR } } @@ -418,93 +437,92 @@ class SPF { for (const addr of addrs) { if (cidr) { // CIDR - const range = ipaddr.parse(addr); + const range = ipaddr.parse(addr) if (this.ipaddr.match(range, cidr)) { - this.log_debug(`mech_a: ${this.ip} => ${addr}/${cidr}: MATCH!`); + this.log_debug(`mech_a: ${this.ip} => ${addr}/${cidr}: MATCH!`) return this.return_const(qualifier) + } else { + this.log_debug(`mech_a: ${this.ip} => ${addr}/${cidr}: NO MATCH`) } - else { - this.log_debug(`mech_a: ${this.ip} => ${addr}/${cidr}: NO MATCH`); - } - } - else { + } else { if (addr === this.ip) { return this.return_const(qualifier) - } - else { - this.log_debug(`mech_a: ${this.ip} => ${addr}: NO MATCH`); + } else { + this.log_debug(`mech_a: ${this.ip} => ${addr}: NO MATCH`) } } } return this.SPF_NONE } - async mech_mx (qualifier, args) { - this.count++; + async mech_mx(qualifier, args) { + this.count++ // Parse any arguments - let cm; - let cidr4; - let cidr6; + let cm + let cidr4 + let cidr6 if (args && (cm = /\/(\d+)((?:\/\/(\d+))?)$/.exec(args))) { - cidr4 = cm[1]; - cidr6 = cm[2]; + cidr4 = cm[1] + cidr6 = cm[2] } - let dm; - let domain = this.domain; + let dm + let domain = this.domain if (args && (dm = /^:([^/ ]+)/.exec(args))) { - domain = dm[1]; + domain = dm[1] } // Fetch the MX records for the specified domain let mxes try { mxes = await net_utils.get_mx(domain) - } - catch (err) { + mxes = mxes.filter((mx) => !net.isIP(mx.exchange)) // remove implicit MX + } catch (err) { switch (err.code) { case dns.NOTFOUND: case dns.NODATA: - case dns.NXDOMAIN: return this.SPF_NONE - default: return this.SPF_TEMPERROR + case dns.NXDOMAIN: + return this.SPF_NONE + default: + return this.SPF_TEMPERROR } } - let pending = 0; - let addresses = []; + let pending = 0 + let addresses = [] // RFC 4408 Section 10.1 if (mxes.length > this.LIMIT) return this.SPF_PERMERROR for (const element of mxes) { - pending++; - const mx = element.exchange; + pending++ + const mx = element.exchange // Calculate which IP method to use - let resolve_method; - let cidr; + let resolve_method + let cidr if (this.ip_ver === 'ipv4') { - cidr = cidr4; - resolve_method = 'resolve4'; - } - else if (this.ip_ver === 'ipv6') { - cidr = cidr6; - resolve_method = 'resolve6'; + cidr = cidr4 + resolve_method = 'resolve4' + } else if (this.ip_ver === 'ipv6') { + cidr = cidr6 + resolve_method = 'resolve6' } let addrs try { addrs = await dns[resolve_method](mx) - } - catch (err) { + } catch (err) { switch (err.code) { case dns.NOTFOUND: case dns.NODATA: - case dns.NXDOMAIN: break; - default: return this.SPF_TEMPERROR + case dns.NXDOMAIN: + break + default: + return this.SPF_TEMPERROR } } - pending--; + pending-- if (addrs) { - this.log_debug(`mech_mx: mx=${mx} addresses=${addrs.join(',')}`); - addresses = addrs.concat(addresses); + this.log_debug(`mech_mx: mx=${mx} addresses=${addrs.join(',')}`) + addresses = addrs.concat(addresses) } if (pending === 0) { if (!addresses.length) return this.SPF_NONE @@ -512,25 +530,30 @@ class SPF { if (cidr) { // CIDR match type for (const address of addresses) { - const range = ipaddr.parse(address); + const range = ipaddr.parse(address) if (this.ipaddr.match(range, cidr)) { - this.log_debug(`mech_mx: ${this.ip} => ${address}/${cidr}: MATCH!`); + this.log_debug( + `mech_mx: ${this.ip} => ${address}/${cidr}: MATCH!`, + ) return this.return_const(qualifier) - } - else { - this.log_debug(`mech_mx: ${this.ip} => ${address}/${cidr}: NO MATCH`); + } else { + this.log_debug( + `mech_mx: ${this.ip} => ${address}/${cidr}: NO MATCH`, + ) } } // No matches return this.SPF_NONE - } - else { + } else { if (addresses.includes(this.ip)) { - this.log_debug(`mech_mx: ${this.ip} => ${addresses.join(',')}: MATCH!`); + this.log_debug( + `mech_mx: ${this.ip} => ${addresses.join(',')}: MATCH!`, + ) return this.return_const(qualifier) - } - else { - this.log_debug(`mech_mx: ${this.ip} => ${addresses.join(',')}: NO MATCH`); + } else { + this.log_debug( + `mech_mx: ${this.ip} => ${addresses.join(',')}: NO MATCH`, + ) return this.SPF_NONE } } @@ -541,47 +564,45 @@ class SPF { if (pending === 0) this.SPF_NONE } - async mech_ptr (qualifier, args) { - this.count++; - let dm; - let domain = this.domain; + async mech_ptr(qualifier, args) { + this.count++ + let dm + let domain = this.domain if (args && (dm = /^:([^/ ]+)/.exec(args))) { - domain = dm[1]; + domain = dm[1] } // First do a PTR lookup for the connecting IP let ptrs try { ptrs = await dns.reverse(this.ip) - } - catch (err) { - this.log_debug(`mech_ptr: lookup=${this.ip} => ${err}`); + } catch (err) { + this.log_debug(`mech_ptr: lookup=${this.ip} => ${err}`) return this.SPF_NONE } - let resolve_method; - if (this.ip_ver === 'ipv4') resolve_method = 'resolve4'; - if (this.ip_ver === 'ipv6') resolve_method = 'resolve6'; - const names = []; + let resolve_method + if (this.ip_ver === 'ipv4') resolve_method = 'resolve4' + if (this.ip_ver === 'ipv6') resolve_method = 'resolve6' + const names = [] // RFC 4408 Section 10.1 if (ptrs.length > this.LIMIT) return this.SPF_PERMERROR for (const ptr of ptrs) { - try { const addrs = await dns[resolve_method](ptr) for (const addr of addrs) { if (addr === this.ip) { - this.log_debug(`mech_ptr: ${this.ip} => ${ptr} => ${addr}: MATCH!`); - names.push(ptr.toLowerCase()); - } - else { - this.log_debug(`mech_ptr: ${this.ip} => ${ptr} => ${addr}: NO MATCH`); + this.log_debug(`mech_ptr: ${this.ip} => ${ptr} => ${addr}: MATCH!`) + names.push(ptr.toLowerCase()) + } else { + this.log_debug( + `mech_ptr: ${this.ip} => ${ptr} => ${addr}: NO MATCH`, + ) } } - } - catch (err) { + } catch (err) { // Skip on error - this.log_debug(`mech_ptr: lookup=${ptr} => ${err}`); + this.log_debug(`mech_ptr: lookup=${ptr} => ${err}`) continue } } @@ -590,27 +611,25 @@ class SPF { // Catch bogus PTR matches e.g. ptr:*.bahnhof.se (should be ptr:bahnhof.se) // These will cause a regexp error, so we can catch them. try { - const re = new RegExp(`${domain.replace('.','\\.')}$`, 'i'); + const re = new RegExp(`${domain.replace('.', '\\.')}$`, 'i') for (const name of names) { if (re.test(name)) { - this.log_debug(`mech_ptr: ${name} => ${domain}: MATCH!`); + this.log_debug(`mech_ptr: ${name} => ${domain}: MATCH!`) return this.return_const(qualifier) - } - else { - this.log_debug(`mech_ptr: ${name} => ${domain}: NO MATCH`); + } else { + this.log_debug(`mech_ptr: ${name} => ${domain}: NO MATCH`) } } return this.SPF_NONE - } - catch (e) { - this.log_debug('mech_ptr', { domain: this.domain, err: e.message }); + } catch (e) { + this.log_debug('mech_ptr', { domain: this.domain, err: e.message }) return this.SPF_PERMERROR } } - async mech_ip (qualifier, args) { - const cidr = args.substr(1); - const match = /^([^/ ]+)(?:\/(\d+))?$/.exec(cidr); + async mech_ip(qualifier, args) { + const cidr = args.substr(1) + const match = /^([^/ ]+)(?:\/(\d+))?$/.exec(cidr) if (!match) return this.SPF_NONE // match[1] == ip @@ -618,49 +637,47 @@ class SPF { try { if (!match[2]) { // Default masks for each IP version - if (this.ip_ver === 'ipv4') match[2] = '32'; - if (this.ip_ver === 'ipv6') match[2] = '128'; + if (this.ip_ver === 'ipv4') match[2] = '32' + if (this.ip_ver === 'ipv6') match[2] = '128' } - const range = ipaddr.parse(match[1]); - const rtype = range.kind(); + const range = ipaddr.parse(match[1]) + const rtype = range.kind() if (this.ip_ver !== rtype) { - this.log_debug(`mech_ip: ${this.ip} => ${cidr}: SKIP`); + this.log_debug(`mech_ip: ${this.ip} => ${cidr}: SKIP`) return this.SPF_NONE } if (this.ipaddr.match(range, match[2])) { - this.log_debug(`mech_ip: ${this.ip} => ${cidr}: MATCH!`); + this.log_debug(`mech_ip: ${this.ip} => ${cidr}: MATCH!`) return this.return_const(qualifier) + } else { + this.log_debug(`mech_ip: ${this.ip} => ${cidr}: NO MATCH`) } - else { - this.log_debug(`mech_ip: ${this.ip} => ${cidr}: NO MATCH`); - } - } - catch (e) { - this.log_debug(e.message); + } catch (e) { + this.log_debug(e.message) return this.SPF_PERMERROR } return this.SPF_NONE } - async mod_redirect (domain) { + async mod_redirect(domain) { // Avoid circular references if (this.been_there[domain]) { - this.log_debug(`circular reference detected: ${domain}`); + this.log_debug(`circular reference detected: ${domain}`) return this.SPF_NONE } - this.count++; - this.been_there[domain] = 1; - return await this.check_host(this.ip, domain, this.mail_from); + this.count++ + this.been_there[domain] = 1 + return await this.check_host(this.ip, domain, this.mail_from) } - async mod_exp (str) { + async mod_exp() { // NOT IMPLEMENTED return this.SPF_NONE } - async mod_v (str) { + async mod_v() { return this.SPF_NONE } } -exports.SPF = SPF; +exports.SPF = SPF diff --git a/package.json b/package.json index 1d2f58b..f40f089 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,23 @@ { "name": "haraka-plugin-spf", - "version": "1.2.4", + "version": "1.2.5", "description": "Sender Policy Framework (SPF) plugin for Haraka", "main": "index.js", + "files": [ + "CHANGELOG.md", + "bin", + "config", + "lib" + ], "scripts": { - "lint": "npx eslint *.js bin/spf lib test", - "lintfix": "npx eslint --fix *.js bin/spf lib test", + "format": "npm run prettier:fix && npm run lint:fix", + "lint": "npx eslint@^8 *.js bin/spf lib test", + "lint:fix": "npx eslint@^8 --fix *.js bin/spf lib test", + "prettier": "npx prettier . --check", + "prettier:fix": "npx prettier . --write --log-level=warn", + "test": "npx mocha@10", "versions": "npx dependency-version-checker check", - "test": "npx mocha" + "versions:fix": "npx dependency-version-checker update && npm run prettier:fix" }, "repository": { "type": "git", @@ -15,6 +25,7 @@ }, "keywords": [ "haraka", + "haraka-plugin", "plugin", "spf" ], @@ -28,15 +39,13 @@ "spf": "./bin/spf" }, "dependencies": { - "haraka-dsn": "^1.0.4", - "haraka-net-utils": "^1.5.3", + "haraka-dsn": "^1.0.5", + "haraka-net-utils": "^1.6.0", "ipaddr.js": "^2.1.0", "nopt": "^7.2.0" }, "devDependencies": { - "eslint": "^8.56.0", - "eslint-plugin-haraka": "*", - "haraka-test-fixtures": "^1.3.3", - "mocha": "^10.2.0" + "@haraka/eslint-config": "^1.1.3", + "haraka-test-fixtures": "^1.3.7" } } diff --git a/test/index.js b/test/index.js index 39a7171..4afda86 100644 --- a/test/index.js +++ b/test/index.js @@ -1,26 +1,25 @@ - // node.js built-in modules -const assert = require('assert') +const assert = require('assert') // npm modules -const Address = require('address-rfc2821').Address; -const constants = require('haraka-constants'); -const fixtures = require('haraka-test-fixtures') +const Address = require('address-rfc2821').Address +const constants = require('haraka-constants') +const fixtures = require('haraka-test-fixtures') -const SPF = require('../lib/spf').SPF; -const spf = new SPF(); +const SPF = require('../lib/spf').SPF +const spf = new SPF() beforeEach(function () { this.plugin = new fixtures.plugin('spf') - this.plugin.timeout = 8000; - this.plugin.load_spf_ini(); + this.plugin.timeout = 8000 + this.plugin.load_spf_ini() // comment this line to see detailed SPF evaluation - this.plugin.SPF.prototype.log_debug = () => {}; + this.plugin.SPF.prototype.log_debug = () => {} - this.connection = fixtures.connection.createConnection(); - this.connection.transaction = fixtures.transaction.createTransaction(); + this.connection = fixtures.connection.createConnection() + this.connection.init_transaction() }) describe('spf', function () { @@ -38,216 +37,336 @@ describe('load_spf_ini', function () { describe('return_results', function () { it('result, none, reject=false', function (done) { - this.plugin.cfg.deny.mfrom_none=false; - this.plugin.return_results(function next () { - assert.equal(undefined, arguments[0]); - done() - }, this.connection, spf, 'mfrom', spf.SPF_NONE, 'test@example.com'); + this.plugin.cfg.deny.mfrom_none = false + this.plugin.return_results( + function next() { + assert.equal(undefined, arguments[0]) + done() + }, + this.connection, + spf, + 'mfrom', + spf.SPF_NONE, + 'test@example.com', + ) }) it('result, none, reject=true', function (done) { - - this.plugin.cfg.deny.mfrom_none=true; - this.plugin.return_results(function next () { - assert.equal(DENY, arguments[0]); - done() - }, this.connection, spf, 'mfrom', spf.SPF_NONE, 'test@example.com'); + this.plugin.cfg.deny.mfrom_none = true + this.plugin.return_results( + function next() { + assert.equal(DENY, arguments[0]) + done() + }, + this.connection, + spf, + 'mfrom', + spf.SPF_NONE, + 'test@example.com', + ) }) it('result, neutral', function (done) { - this.plugin.return_results(function next () { - assert.equal(undefined, arguments[0]); - done() - }, this.connection, spf, 'mfrom', spf.SPF_NEUTRAL, 'test@example.com'); + this.plugin.return_results( + function next() { + assert.equal(undefined, arguments[0]) + done() + }, + this.connection, + spf, + 'mfrom', + spf.SPF_NEUTRAL, + 'test@example.com', + ) }) it('result, pass', function (done) { - this.plugin.return_results(function next () { - assert.equal(undefined, arguments[0]); - done() - }, this.connection, spf, 'mfrom', spf.SPF_PASS, 'test@example.com'); + this.plugin.return_results( + function next() { + assert.equal(undefined, arguments[0]) + done() + }, + this.connection, + spf, + 'mfrom', + spf.SPF_PASS, + 'test@example.com', + ) }) it('result, softfail, reject=false', function (done) { - this.plugin.cfg.deny.mfrom_softfail=false; - this.plugin.return_results(function next () { - assert.equal(undefined, arguments[0]); - done() - }, this.connection, spf, 'mfrom', spf.SPF_SOFTFAIL, 'test@example.com'); + this.plugin.cfg.deny.mfrom_softfail = false + this.plugin.return_results( + function next() { + assert.equal(undefined, arguments[0]) + done() + }, + this.connection, + spf, + 'mfrom', + spf.SPF_SOFTFAIL, + 'test@example.com', + ) }) it('result, softfail, reject=true', function (done) { - this.plugin.cfg.deny.mfrom_softfail=true; - this.plugin.return_results(function next () { - assert.equal(DENY, arguments[0]); - done() - }, this.connection, spf, 'mfrom', spf.SPF_SOFTFAIL, 'test@example.com'); + this.plugin.cfg.deny.mfrom_softfail = true + this.plugin.return_results( + function next() { + assert.equal(DENY, arguments[0]) + done() + }, + this.connection, + spf, + 'mfrom', + spf.SPF_SOFTFAIL, + 'test@example.com', + ) }) it('result, fail, reject=false', function (done) { - this.plugin.cfg.deny.mfrom_fail=false; - this.plugin.return_results(function next () { - assert.equal(undefined, arguments[0]); - done() - }, this.connection, spf, 'mfrom', spf.SPF_FAIL, 'test@example.com'); + this.plugin.cfg.deny.mfrom_fail = false + this.plugin.return_results( + function next() { + assert.equal(undefined, arguments[0]) + done() + }, + this.connection, + spf, + 'mfrom', + spf.SPF_FAIL, + 'test@example.com', + ) }) it('result, fail, reject=true', function (done) { - this.plugin.cfg.deny.mfrom_fail=true; - this.plugin.return_results(function next () { - assert.equal(DENY, arguments[0]); - done() - }, this.connection, spf, 'mfrom', spf.SPF_FAIL, 'test@example.com'); + this.plugin.cfg.deny.mfrom_fail = true + this.plugin.return_results( + function next() { + assert.equal(DENY, arguments[0]) + done() + }, + this.connection, + spf, + 'mfrom', + spf.SPF_FAIL, + 'test@example.com', + ) }) it('result, temperror, reject=false', function (done) { - this.plugin.cfg.defer.mfrom_temperror=false; - this.plugin.return_results(function next () { - assert.equal(undefined, arguments[0]); - done() - }, this.connection, spf, 'mfrom', spf.SPF_TEMPERROR, 'test@example.com'); + this.plugin.cfg.defer.mfrom_temperror = false + this.plugin.return_results( + function next() { + assert.equal(undefined, arguments[0]) + done() + }, + this.connection, + spf, + 'mfrom', + spf.SPF_TEMPERROR, + 'test@example.com', + ) }) it('result, temperror, reject=true', function (done) { - this.plugin.cfg.defer.mfrom_temperror=true; - this.plugin.return_results(function next () { - assert.equal(DENYSOFT, arguments[0]); - done() - }, this.connection, spf, 'mfrom', spf.SPF_TEMPERROR, 'test@example.com'); + this.plugin.cfg.defer.mfrom_temperror = true + this.plugin.return_results( + function next() { + assert.equal(DENYSOFT, arguments[0]) + done() + }, + this.connection, + spf, + 'mfrom', + spf.SPF_TEMPERROR, + 'test@example.com', + ) }) it('result, permerror, reject=false', function (done) { - this.plugin.cfg.deny.mfrom_permerror=false; - this.plugin.return_results(function next () { - assert.equal(undefined, arguments[0]); - done() - }, this.connection, spf, 'mfrom', spf.SPF_PERMERROR, 'test@example.com'); + this.plugin.cfg.deny.mfrom_permerror = false + this.plugin.return_results( + function next() { + assert.equal(undefined, arguments[0]) + done() + }, + this.connection, + spf, + 'mfrom', + spf.SPF_PERMERROR, + 'test@example.com', + ) }) it('result, permerror, reject=true', function (done) { - this.plugin.cfg.deny.mfrom_permerror=true; - this.plugin.return_results( function next () { - assert.equal(DENY, arguments[0]); - done() - }, this.connection, spf, 'mfrom', spf.SPF_PERMERROR, 'test@example.com'); + this.plugin.cfg.deny.mfrom_permerror = true + this.plugin.return_results( + function next() { + assert.equal(DENY, arguments[0]) + done() + }, + this.connection, + spf, + 'mfrom', + spf.SPF_PERMERROR, + 'test@example.com', + ) }) it('result, unknown', function (done) { - this.plugin.return_results(function next () { - assert.equal(undefined, arguments[0]); - done() - }, this.connection, spf, 'mfrom', 'unknown', 'test@example.com'); + this.plugin.return_results( + function next() { + assert.equal(undefined, arguments[0]) + done() + }, + this.connection, + spf, + 'mfrom', + 'unknown', + 'test@example.com', + ) }) }) describe('hook_helo', function () { it('rfc1918', function (done) { - let completed = 0; - function next (rc) { - completed++; - assert.equal(undefined, rc); + let completed = 0 + function next(rc) { + completed++ + assert.equal(undefined, rc) if (completed >= 2) done() } - this.connection.remote.is_private=true; - this.plugin.helo_spf(next, this.connection); - this.plugin.helo_spf(next, this.connection, 'helo.sender.com'); + this.connection.remote.is_private = true + this.plugin.helo_spf(next, this.connection) + this.plugin.helo_spf(next, this.connection, 'helo.sender.com') }) it('IPv4 literal', function (done) { - this.connection.remote.ip='190.168.1.1'; - this.plugin.helo_spf(function next (rc) { - assert.equal(undefined, rc); - done() - }, this.connection, '[190.168.1.1]' ); + this.connection.remote.ip = '190.168.1.1' + this.plugin.helo_spf( + function next(rc) { + assert.equal(undefined, rc) + done() + }, + this.connection, + '[190.168.1.1]', + ) }) it('MX with no A record', function (done) { this.timeout(5000) - this.connection.set('remote.ip', '192.0.2.0'); - this.plugin.helo_spf(function next (rc) { - assert.equal(undefined, rc); - done() - }, this.connection, 'haraka-test.tnpi.net' ); + this.connection.set('remote.ip', '192.0.2.0') + this.plugin.helo_spf( + function next(rc) { + assert.equal(undefined, rc) + done() + }, + this.connection, + 'test.haraka.tnpi.net', + ) }) }) -const test_addr = new Address(''); +const test_addr = new Address('') describe('hook_mail', function () { + this.timeout(5000) it('rfc1918', function (done) { - this.connection.set('remote.is_private', true); - this.connection.set('remote.ip', '192.168.1.1'); - this.plugin.hook_mail(function next () { - assert.equal(undefined, arguments[0]); - done() - }, this.connection, [test_addr]); + this.connection.set('remote.is_private', true) + this.connection.set('remote.ip', '192.168.1.1') + this.plugin.hook_mail( + function next() { + assert.equal(undefined, arguments[0]) + done() + }, + this.connection, + [test_addr], + ) }) it('rfc1918 relaying', function (done) { - this.connection.set('remote.is_private', true); - this.connection.set('remote.ip','192.168.1.1'); - this.connection.relaying=true; - this.plugin.hook_mail(function next () { - assert.ok([undefined, constants.CONT].includes(arguments[0])); - done() - }, this.connection, [test_addr]); + this.connection.set('remote.is_private', true) + this.connection.set('remote.ip', '192.168.1.1') + this.connection.relaying = true + this.plugin.hook_mail( + function next() { + assert.ok([undefined, constants.CONT].includes(arguments[0])) + done() + }, + this.connection, + [test_addr], + ) }) it('no txn', function (done) { - this.connection.remote.ip='207.85.1.1'; - delete this.connection.transaction; - this.plugin.hook_mail(function next () { - assert.equal(undefined, arguments[0]); - assert.equal(undefined, arguments[1]); + this.connection.remote.ip = '207.85.1.1' + delete this.connection.transaction + this.plugin.hook_mail(function next() { + assert.equal(undefined, arguments[0]) + assert.equal(undefined, arguments[1]) done() - }, this.connection); + }, this.connection) }) it('txn, no helo', function (done) { - this.timeout(3000) - this.plugin.cfg.deny.mfrom_fail = false; - this.connection.set('remote.ip', '207.85.1.1'); - this.plugin.hook_mail(function next () { - assert.equal(undefined, arguments[0]); - assert.equal(undefined, arguments[1]); - done() - }, this.connection, [test_addr]); + this.plugin.cfg.deny.mfrom_fail = false + this.connection.set('remote.ip', '207.85.1.1') + this.plugin.hook_mail( + function next() { + assert.equal(undefined, arguments[0]) + assert.equal(undefined, arguments[1]) + done() + }, + this.connection, + [test_addr], + ) }) it('txn', function (done) { - this.timeout(3000) - this.connection.set('remote.ip', '207.85.1.1'); - this.connection.set('hello.host', 'mail.example.com'); - this.plugin.hook_mail(function next (rc) { - assert.equal(undefined, rc); - done() - }, this.connection, [test_addr]); + this.connection.set('remote.ip', '207.85.1.1') + this.connection.set('hello.host', 'mail.example.com') + this.plugin.hook_mail( + function next(rc) { + assert.equal(undefined, rc) + done() + }, + this.connection, + [test_addr], + ) }) it('txn, relaying', function (done) { - this.timeout(3000) - this.connection.set('remote.ip', '207.85.1.1'); - this.connection.set('relaying', true); - this.connection.set('hello.host', 'mail.example.com'); - this.plugin.hook_mail(function next (rc) { - assert.equal(undefined, rc); - done() - }, this.connection, [test_addr]); + this.connection.set('remote.ip', '207.85.1.1') + this.connection.set('relaying', true) + this.connection.set('hello.host', 'mail.example.com') + this.plugin.hook_mail( + function next(rc) { + assert.equal(undefined, rc) + done() + }, + this.connection, + [test_addr], + ) }) it('txn, relaying, is_private', function (done) { - this.timeout(8000) - this.plugin.cfg.relay.context='myself'; - this.plugin.cfg.deny_relay.mfrom_fail = true; - this.connection.set('remote.ip', '127.0.1.1'); - this.connection.set('remote.is_private', true); - this.connection.relaying = true; - this.connection.set('hello.host', 'www.tnpi.net'); - this.plugin.nu.public_ip = '66.128.51.165'; - this.plugin.hook_mail(function next (rc) { - assert.equal(undefined, rc); - done() - }, this.connection, [new Address('')]); + this.timeout(12000) + this.plugin.cfg.relay.context = 'myself' + this.plugin.cfg.deny_relay.mfrom_fail = true + this.connection.set('remote.ip', '127.0.1.1') + this.connection.set('remote.is_private', true) + this.connection.relaying = true + this.connection.set('hello.host', 'www.tnpi.net') + this.plugin.nu.public_ip = '66.128.51.165' + this.plugin.hook_mail( + function next(rc) { + assert.equal(undefined, rc) + done() + }, + this.connection, + [new Address('')], + ) }) }) diff --git a/test/spf.js b/test/spf.js index e64a2c8..a260fa3 100644 --- a/test/spf.js +++ b/test/spf.js @@ -1,10 +1,8 @@ const assert = require('assert') -const SPF = require('../lib/spf').SPF; - -SPF.prototype.log_debug = () => {}; // noop, hush debug output - +const SPF = require('../lib/spf').SPF +SPF.prototype.log_debug = () => {} // noop, hush debug output beforeEach(function () { this.SPF = new SPF() @@ -12,32 +10,32 @@ beforeEach(function () { describe('SPF', function () { it('new SPF', function () { - assert.ok(this.SPF); + assert.ok(this.SPF) }) it('constants', function () { - assert.equal(1, this.SPF.SPF_NONE); - assert.equal(2, this.SPF.SPF_PASS); - assert.equal(3, this.SPF.SPF_FAIL); - assert.equal(4, this.SPF.SPF_SOFTFAIL); - assert.equal(5, this.SPF.SPF_NEUTRAL); - assert.equal(6, this.SPF.SPF_TEMPERROR); - assert.equal(7, this.SPF.SPF_PERMERROR); - assert.equal(10, this.SPF.LIMIT); + assert.equal(1, this.SPF.SPF_NONE) + assert.equal(2, this.SPF.SPF_PASS) + assert.equal(3, this.SPF.SPF_FAIL) + assert.equal(4, this.SPF.SPF_SOFTFAIL) + assert.equal(5, this.SPF.SPF_NEUTRAL) + assert.equal(6, this.SPF.SPF_TEMPERROR) + assert.equal(7, this.SPF.SPF_PERMERROR) + assert.equal(10, this.SPF.LIMIT) }) it('mod_redirect, true', async function () { - this.SPF.been_there['example.com'] = true; + this.SPF.been_there['example.com'] = true const rc = await this.SPF.mod_redirect('example.com') // assert.equal(null, err); - assert.equal(1, rc); + assert.equal(1, rc) }) it('mod_redirect, false', async function () { - this.timeout=4000 - this.SPF.count=0; - this.SPF.ip='212.70.129.94'; - this.SPF.mail_from='fraud@aexp.com'; + this.timeout = 4000 + this.SPF.count = 0 + this.SPF.ip = '212.70.129.94' + this.SPF.mail_from = 'fraud@aexp.com' const rc = await this.SPF.mod_redirect('aexp.com') switch (rc) { @@ -45,53 +43,59 @@ describe('SPF', function () { // from time to time (this is the third time we've seen it, // American Express publishes an invalid SPF record which results // in a PERMERROR. Ignore it. - assert.equal(rc, 7, "aexp SPF record is broken again"); - break; + assert.equal(rc, 7, 'aexp SPF record is broken again') + break case 6: - assert.equal(rc, 6, "temporary (likely DNS timeout) error"); - break; + assert.equal(rc, 6, 'temporary (likely DNS timeout) error') + break default: - assert.equal(rc, 3); + assert.equal(rc, 3) } }) it('check_host, gmail.com, fail', async function () { - this.timeout = 3000; - this.SPF.count=0; - const rc = await this.SPF.check_host('212.70.129.94', 'gmail.com', 'haraka.mail@gmail.com') + this.timeout = 3000 + this.SPF.count = 0 + const rc = await this.SPF.check_host( + '212.70.129.94', + 'gmail.com', + 'haraka.mail@gmail.com', + ) switch (rc) { case 1: - assert.equal(rc, 1, "none"); - console.log('Why do DNS lookup fail to find gmail SPF record on GitHub Actions?'); - break; + assert.equal(rc, 1, 'none') + console.log( + 'Why do DNS lookup fail to find gmail SPF record on GitHub Actions?', + ) + break case 3: - assert.equal(rc, 3, "fail"); - break; + assert.equal(rc, 3, 'fail') + break case 4: - assert.equal(rc, 4, "soft fail"); - break; + assert.equal(rc, 4, 'soft fail') + break case 7: - assert.equal(rc, 7, "perm error"); - break; + assert.equal(rc, 7, 'perm error') + break default: assert.equal(rc, 4) } }) it('check_host, facebook.com, pass', async function () { - this.timeout = 3000; - this.SPF.count = 0; - const rc = await this.SPF.check_host('69.171.232.145', 'facebookmail.com'); - assert.equal(rc, this.SPF.SPF_PASS, "pass"); + this.timeout = 3000 + this.SPF.count = 0 + const rc = await this.SPF.check_host('69.171.232.145', 'facebookmail.com') + assert.equal(rc, this.SPF.SPF_PASS, 'pass') }) it('valid_ip, true', function (done) { - assert.equal(this.SPF.valid_ip(':212.70.129.94'), true); + assert.equal(this.SPF.valid_ip(':212.70.129.94'), true) done() }) it('valid_ip, false', function (done) { - assert.equal(this.SPF.valid_ip(':212.70.d.94'), false); + assert.equal(this.SPF.valid_ip(':212.70.d.94'), false) done() }) })