diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a337c42..12d7556 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,6 @@ name: CI -on: [ push ] +on: [ push, pull_request ] env: CI: true @@ -10,42 +10,11 @@ jobs: lint: uses: haraka/.github/.github/workflows/lint.yml@master - test: - needs: lint - runs-on: ${{ matrix.os }} - services: - redis: - image: redis - ports: - - 6379:6379 - strategy: - matrix: - os: [ ubuntu-latest ] - node-version: [ 14, 16, 18 ] - fail-fast: false - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - name: Node ${{ matrix.node-version }} on ${{ matrix.os }} - with: - node-version: ${{ matrix.node-version }} - - run: npm install - - run: npm test + ubuntu: + needs: [ lint ] + uses: haraka/.github/.github/workflows/ubuntu.yml@master - test-win: - needs: lint - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ windows-latest ] - node-version: [ 14, 16, 18 ] - fail-fast: false - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - name: Node ${{ matrix.node-version }} on ${{ matrix.os }} - with: - node-version: ${{ matrix.node-version }} - - run: npm install - - run: npm test - if: ${{ false }} # disabled, until Redis for GHA Windows exists + windows: + needs: [ lint ] + uses: haraka/.github/.github/workflows/windows.yml@master + if: ${{ false }} # disabled, until Redis for GHA Windows exists \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5f17632..9bfdec3 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -4,48 +4,14 @@ on: push: branches: - master + paths: + - package.json env: CI: true - node-version: 16 jobs: - build: - runs-on: ubuntu-latest - services: - redis: - image: redis - ports: - - 6379:6379 - steps: - - uses: actions/setup-node@v3 - name: Node ${{ env.node-version }} - with: - node-version: ${{ env.node-version }} - - uses: actions/checkout@v3 - - run: npm install - - run: npm test + publish: + uses: haraka/.github/.github/workflows/publish.yml@master + secrets: inherit - publish-npm: - needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/setup-node@v3 - name: Node ${{ env.node-version }} - with: - node-version: ${{ env.node-version }} - registry-url: https://registry.npmjs.org/ - - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - # fetch-depth 0 needed by GitHub Release - - - name: publish to NPM - run: npm publish - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} - - - name: GitHub Release - uses: justincy/github-action-npm-release@2.0.1 - id: release \ No newline at end of file diff --git a/.release b/.release index 4d5c90b..0890e94 160000 --- a/.release +++ b/.release @@ -1 +1 @@ -Subproject commit 4d5c90b2c428d2cd9d5c35758b0071925f5a0f0a +Subproject commit 0890e945e4e061c96c7b2ab45017525904c17728 diff --git a/Changes.md b/Changes.md index da89884..aab59ca 100644 --- a/Changes.md +++ b/Changes.md @@ -2,6 +2,14 @@ #### N.N.N - YYYY-MM-DD +### [1.1.0] - 2023-12-12 + +- feat(ignored_ods): ignore specified org domains +- style(es6): replace for i with for...of +- ci(publish): only when package.json modified +- ci: use shared workflows + + #### 1.0.9 - 2022-05-28 - restore a plugin = this, context demands it @@ -51,3 +59,5 @@ ### 1.0.2 - 2016-02-06 - inherit from haraka-plugin-redis (vs redis) + +[1.1.0]: https://github.com/haraka/haraka-plugin-known-senders/releases/tag/1.1.0 diff --git a/README.md b/README.md index a8dfdbc..7c08703 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ [![Build Status][ci-img]][ci-url] [![Code Climate][clim-img]][clim-url] + [![NPM][npm-img]][npm-url] # haraka-plugin-known-senders @@ -50,8 +51,8 @@ This plugin can boost the reputation of most marginally deliverable ham. Where i -[ci-img]: https://travis-ci.org/haraka/haraka-plugin-known-senders.svg?branch=master -[ci-url]: https://travis-ci.org/haraka/haraka-plugin-known-senders +[ci-img]: https://github.com/haraka/haraka-plugin-known-senders/actions/workflows/ci.yml/badge.svg +[ci-url]: https://github.com/haraka/haraka-plugin-known-senders/actions/workflows/ci.yml [clim-img]: https://codeclimate.com/github/haraka/haraka-plugin-known-senders/badges/gpa.svg [clim-url]: https://codeclimate.com/github/haraka/haraka-plugin-known-senders [npm-img]: https://nodei.co/npm/haraka-plugin-known-senders.png diff --git a/config/known-senders.ini b/config/known-senders.ini index aad613e..358f5d6 100644 --- a/config/known-senders.ini +++ b/config/known-senders.ini @@ -5,3 +5,12 @@ ; port = 6379 ; db = 3 +[ignored_ods] +gmail.com +hotmail.com +outlook.com +aol.com +yahoo.com +icloud.com +me.com +mac.com \ No newline at end of file diff --git a/index.js b/index.js index 036c27d..a3e7453 100644 --- a/index.js +++ b/index.js @@ -23,6 +23,8 @@ exports.load_sender_ini = function () { plugin.load_sender_ini(); }); + if (plugin.cfg.ignored_ods === undefined) plugin.cfg.ignored_ods = {} + plugin.merge_redis_ini(); } @@ -36,6 +38,7 @@ exports.load_sender_ini = function () { */ exports.update_sender = async function (next, connection, params) { + const plugin = this; // queue_ok arguments: next, connection, msg // ok 1390590369 qp 634 (F82E2DD5-9238-41DC-BC95-9C3A02716AD2.1) @@ -43,7 +46,7 @@ exports.update_sender = async function (next, connection, params) { let rcpt_domains; function errNext (err) { - connection.logerror(this, `update_sender: ${err}`); + connection.logerror(plugin, `update_sender: ${err}`); next(null, null, sender_od, rcpt_domains); } @@ -55,6 +58,7 @@ exports.update_sender = async function (next, connection, params) { sender_od = this.get_sender_domain_by_txn(txn); if (!sender_od) return errNext('no sender domain'); + if (sender_od in plugin.cfg.ignored_ods) return errNext(`ignored(${sender_od})`); rcpt_domains = this.get_recipient_domains_by_txn(txn); if (rcpt_domains.length === 0) { @@ -65,8 +69,8 @@ exports.update_sender = async function (next, connection, params) { // and the recipient is an external domain try { const multi = this.db.multi(); - for (let i = 0; i < rcpt_domains.length; i++) { - multi.hIncrBy(sender_od, rcpt_domains[i], 1); + for (const rcptDomain of rcpt_domains) { + multi.hIncrBy(sender_od, rcptDomain, 1); } const replies = await multi.exec() @@ -82,13 +86,10 @@ exports.update_sender = async function (next, connection, params) { } exports.get_sender_domain_by_txn = function (txn) { - const plugin = this; - - if (!txn.mail_from) return; - if (!txn.mail_from.host) return; + if (!txn.mail_from || !txn.mail_from.host) return; const sender_od = tlds.get_organizational_domain(txn.mail_from.host); if (txn.mail_from.host !== sender_od) { - plugin.logdebug(`sender: ${txn.mail_from.host} -> ${sender_od}`); + this.logdebug(`sender: ${txn.mail_from.host} -> ${sender_od}`); } return sender_od; } @@ -99,11 +100,11 @@ exports.get_recipient_domains_by_txn = function (txn) { const rcpt_domains = []; if (!txn.rcpt_to) return rcpt_domains; - for (let i=0; i < txn.rcpt_to.length; i++) { - if (!txn.rcpt_to[i].host) continue; - const rcpt_od = tlds.get_organizational_domain(txn.rcpt_to[i].host); - if (txn.rcpt_to[i].host !== rcpt_od) { - plugin.loginfo(`rcpt: ${txn.rcpt_to[i].host} -> ${rcpt_od}`); + for (const element of txn.rcpt_to) { + if (!element.host) continue; + const rcpt_od = tlds.get_organizational_domain(element.host); + if (element.host !== rcpt_od) { + plugin.loginfo(`rcpt: ${element.host} -> ${rcpt_od}`); } if (rcpt_domains.indexOf(rcpt_od) === -1) { // not a duplicate, add to the list @@ -125,19 +126,19 @@ exports.get_recipient_domains_by_txn = function (txn) { // early checks, on the mail hook exports.is_authenticated = function (next, connection, params) { - const plugin = this; // only validate inbound messages if (connection.relaying) return next(); - const sender_od = plugin.get_sender_domain_by_txn(connection.transaction); + const sender_od = this.get_sender_domain_by_txn(connection.transaction); + if (sender_od in this.cfg.ignored_ods) return next() - if (plugin.has_fcrdns_match(sender_od, connection)) { - connection.logdebug(plugin, `+fcrdns: ${sender_od}`); + if (this.has_fcrdns_match(sender_od, connection)) { + connection.logdebug(this, `+fcrdns: ${sender_od}`); return next(null, null, sender_od); } - if (plugin.has_spf_match(sender_od, connection)) { - connection.logdebug(plugin, `+spf: ${sender_od}`); + if (this.has_spf_match(sender_od, connection)) { + connection.logdebug(this, `+spf: ${sender_od}`); return next(null, null, sender_od); } @@ -145,28 +146,25 @@ exports.is_authenticated = function (next, connection, params) { if (connection.tls.verified) { // TODO: get the CN and Subject Alternative Names of the cert // then look for match with sender_od - connection.logdebug(plugin, `+tls: ${sender_od}`); + connection.logdebug(this, `+tls: ${sender_od}`); // return next(null, null, sender_od); } - return next(); + next(); } exports.get_validated_sender_od = function (connection) { - const plugin = this; - if (!connection) return; - if (!connection.transaction) return; - const txn_res = connection.transaction.results.get(plugin.name); + if (!connection || !connection.transaction) return; + const txn_res = connection.transaction.results.get(this.name); if (!txn_res) return; return txn_res.sender; } exports.get_rcpt_ods = function (connection) { - const plugin = this; if (!connection) return []; if (!connection.transaction) return []; - const txn_r = connection.transaction.results.get(plugin.name); + const txn_r = connection.transaction.results.get(this.name); if (!txn_r) return []; return txn_r.rcpt_ods; @@ -201,6 +199,7 @@ exports.check_recipient = async function (next, connection, rcpt) { // if no validated sender domain, there's nothing to do...yet const sender_od = this.get_validated_sender_od(connection); if (!sender_od) return next(); + if (sender_od in this.cfg.ignored_ods) return errNext(`ignored(${sender_od})`) // The sender OD is validated, check Redis for a match try { @@ -236,6 +235,7 @@ exports.is_dkim_authenticated = async function (next, connection) { const sender_od = this.get_validated_sender_od(connection); if (!sender_od) return errNext('no sender_od'); + if (sender_od in this.cfg.ignored_ods) return infoNext(`ignored(${sender_od})`) rcpt_ods = this.get_rcpt_ods(connection); if (!rcpt_ods || ! rcpt_ods.length) return errNext('no rcpt_ods'); @@ -247,12 +247,12 @@ exports.is_dkim_authenticated = async function (next, connection) { try { const multi = this.db.multi(); - for (let i = 0; i < dkim.pass.length; i++) { - const dkim_od = tlds.get_organizational_domain(dkim.pass[i]); + for (const pas of dkim.pass) { + const dkim_od = tlds.get_organizational_domain(pas); if (dkim_od === sender_od) { connection.transaction.results.add(this, { sender: sender_od, auth: 'dkim' }); - for (let j = 0; j < rcpt_ods.length; j++) { - multi.hGet(rcpt_ods[j], sender_od); + for (const rcptOd of rcpt_ods) { + multi.hGet(rcptOd, sender_od); } } } diff --git a/package.json b/package.json index 79ee95a..8b4de78 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,14 @@ { "name": "haraka-plugin-known-senders", - "version": "1.0.9", + "version": "1.1.0", "description": "Increase the reputation of recognized sender domains.", "main": "index.js", "scripts": { "cover": "npx nyc --reporter=lcov --hook-run-in-context npm run test", "lint": "npx eslint *.js test/*.js", "lintfix": "npx eslint --fix *.js test/*.js", - "test": "npx mocha --exit" + "test": "npx mocha --exit", + "versions": "npx dependency-version-checker check" }, "repository": { "type": "git", @@ -28,14 +29,14 @@ }, "homepage": "https://github.com/haraka/haraka-plugin-known-senders#readme", "devDependencies": { - "address-rfc2821": "*", - "eslint": ">=8", - "eslint-plugin-haraka": "*", - "haraka-test-fixtures": "*", - "mocha": ">=9" + "address-rfc2821": "^2.1.1", + "eslint": "^8.55.0", + "eslint-plugin-haraka": "^1.0.15", + "haraka-test-fixtures": "^1.3.3", + "mocha": "^10.2.0" }, "dependencies": { - "haraka-plugin-redis": "^2.0.3", - "haraka-tld": "*" + "haraka-plugin-redis": "^2.0.6", + "haraka-tld": "^1.2.0" } } diff --git a/test/index.js b/test/index.js index 6658aa2..c82c31d 100644 --- a/test/index.js +++ b/test/index.js @@ -4,16 +4,31 @@ const assert = require('assert'); const Address = require('address-rfc2821').Address; const fixtures = require('haraka-test-fixtures'); + +describe('register', function () { + it('registers', function () { + const plugin = new fixtures.plugin('index'); + plugin.register() + }) + + it('loads the config', function () { + const plugin = new fixtures.plugin('index'); + plugin.register() + assert.deepEqual(false, 'simerson.net' in plugin.cfg.ignored_ods) + assert.deepEqual(true, 'gmail.com' in plugin.cfg.ignored_ods) + }) +}) + describe('is_authenticated', function () { const plugin = new fixtures.plugin('index'); + plugin.register(); - beforeEach(function (done) { + beforeEach(function () { this.connection = fixtures.connection.createConnection(); this.connection.results = new fixtures.result_store(this.connection); this.connection.transaction = fixtures.transaction.createTransaction(); this.connection.transaction.results = new fixtures.result_store(this.connection); - done(); }) it('returns empty when no auth found', function (done) { @@ -88,12 +103,9 @@ describe('is_authenticated', function () { describe('check_recipient', function () { let connection; - beforeEach(function (done) { + beforeEach(function () { connection = fixtures.connection.createConnection(); - // connection.relaying = true; connection.transaction = fixtures.transaction.createTransaction(); - connection.transaction.results = new fixtures.result_store(connection); - done(); }) it('reduces domain to OD', function (done) { @@ -119,15 +131,12 @@ describe('update_sender', function () { this.plugin = new fixtures.plugin('index'); this.plugin.inherits('haraka-plugin-redis'); - this.plugin.load_redis_ini() - this.plugin.init_redis_plugin(() => { - done() - }) + this.plugin.load_sender_ini(); + this.plugin.init_redis_plugin(done); }) - after(function (done) { + after(function () { this.plugin.shutdown(); - done() }) it('gets the sender domain', function (done) { @@ -168,81 +177,63 @@ describe('update_sender', function () { }) describe('get_rcpt_ods', function () { - it('always returns an array', function (done) { - done(); + it('always returns an array', function () { }); }) describe('get_sender_domain_by_txn', function () { - beforeEach(function (done) { + beforeEach(function () { this.plugin = new fixtures.plugin('known-senders'); this.connection = new fixtures.connection.createConnection(); this.connection.transaction = new fixtures.transaction.createTransaction(); - this.connection.transaction.results = new fixtures.result_store(this.connection); - done(); }); - it('returns a sender domain: example.com', function (done) { - const plugin = this.plugin; + it('returns a sender domain: example.com', function () { this.connection.transaction.mail_from = new Address(''); - assert.equal(plugin.get_sender_domain_by_txn(this.connection.transaction), 'example.com'); - done(); + assert.equal(this.plugin.get_sender_domain_by_txn(this.connection.transaction), 'example.com'); }); - it('returns a sender domain: mail.example.com', function (done) { - const plugin = this.plugin; + it('returns a sender domain: mail.example.com', function () { this.connection.transaction.mail_from = new Address(''); - assert.equal(plugin.get_sender_domain_by_txn(this.connection.transaction), 'example.com'); - done(); + assert.equal(this.plugin.get_sender_domain_by_txn(this.connection.transaction), 'example.com'); }); - it('returns a sender domain: bbc.co.uk', function (done) { - const plugin = this.plugin; + it('returns a sender domain: bbc.co.uk', function () { this.connection.transaction.mail_from = new Address(''); - assert.equal(plugin.get_sender_domain_by_txn(this.connection.transaction), 'bbc.co.uk'); - done(); + assert.equal(this.plugin.get_sender_domain_by_txn(this.connection.transaction), 'bbc.co.uk'); }); }) describe('get_recipient_domains_by_txn', function () { - beforeEach(function (done) { + beforeEach(function () { this.plugin = new fixtures.plugin('known-senders'); - this.connection = new fixtures.connection.createConnection(); this.connection.transaction = new fixtures.transaction.createTransaction(); - this.connection.transaction.results = new fixtures.result_store(this.connection); - done(); }); - it('retrieves domains from txn recipients: example.com', function (done) { - const plugin = this.plugin; + it('retrieves domains from txn recipients: example.com', function () { const txn = this.connection.transaction; txn.rcpt_to.push(new Address('')); - const rcpt_doms = plugin.get_recipient_domains_by_txn(txn); + const rcpt_doms = this.plugin.get_recipient_domains_by_txn(txn); assert.deepEqual(rcpt_doms, ['example.com'], rcpt_doms); - done(); }); - it('retrieves domains from txn recipients: example[1-2].com', function (done) { - const plugin = this.plugin; + it('retrieves domains from txn recipients: example[1-2].com', function () { const txn = this.connection.transaction; txn.rcpt_to.push(new Address('')); txn.rcpt_to.push(new Address('')); - const rcpt_doms = plugin.get_recipient_domains_by_txn(txn); + const rcpt_doms = this.plugin.get_recipient_domains_by_txn(txn); assert.deepEqual(rcpt_doms, ['example1.com', 'example2.com'], rcpt_doms); - done(); }); - it('retrieves unique domains from txn recipients: example.com', function (done) { - const plugin = this.plugin; + it('retrieves unique domains from txn recipients: example.com', function () { const txn = this.connection.transaction; txn.rcpt_to.push(new Address('')); txn.rcpt_to.push(new Address('')); - const rcpt_doms = plugin.get_recipient_domains_by_txn(txn); + const rcpt_doms = this.plugin.get_recipient_domains_by_txn(txn); assert.deepEqual(rcpt_doms, ['example.com'], rcpt_doms); - done(); }); }) @@ -252,20 +243,14 @@ describe('is_dkim_authenticated', function () { this.connection = new fixtures.connection.createConnection(); this.connection.transaction = new fixtures.transaction.createTransaction(); - this.connection.transaction.results = new fixtures.result_store(this.connection); this.plugin = new fixtures.plugin('known-senders'); - - this.plugin.inherits('haraka-plugin-redis'); - this.plugin.load_redis_ini() - this.plugin.init_redis_plugin(() => { - done() - }) + this.plugin.register(); + this.plugin.init_redis_plugin(done) }) - after(function (done) { + after(function () { this.plugin.shutdown(); - done() }) it('finds dkim results', function (done) {