From 2bd6066a20d1b8599c196b37ea8a7a34d3db34cf Mon Sep 17 00:00:00 2001 From: Matt Simerson Date: Sun, 26 Jan 2025 10:32:53 -0800 Subject: [PATCH] doc(CONTRIBUTORS): added - doc: mv Changes CHANGELOG.md - prefix node built-in modules with node: - test: add disabled test than submits a rspamd req --- .codeclimate.yml | 8 +- .github/PULL_REQUEST_TEMPLATE.md | 6 +- .github/dependabot.yml | 6 +- .github/workflows/ci.yml | 33 +-- .github/workflows/codeql.yml | 6 +- .github/workflows/publish.yml | 2 +- .release | 2 +- Changes.md => CHANGELOG.md | 27 +- CONTRIBUTORS.md | 10 + README.md | 129 +++++---- eslint.config.mjs | 59 ++-- index.js | 458 +++++++++++++++---------------- package.json | 25 +- test/fixtures/spam.eml | 168 ++++++++++++ test/index.js | 287 ++++++++++--------- 15 files changed, 709 insertions(+), 517 deletions(-) rename Changes.md => CHANGELOG.md (78%) create mode 100644 CONTRIBUTORS.md create mode 100644 test/fixtures/spam.eml diff --git a/.codeclimate.yml b/.codeclimate.yml index 0e443ca..563da6b 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,10 +1,10 @@ engines: eslint: enabled: true - channel: "eslint-8" + channel: 'eslint-9' config: - config: ".eslintrc.yaml" + config: 'eslint.config.mjs' ratings: - paths: - - "**.js" + paths: + - '**.js' 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 5360933..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 @@ -14,28 +13,10 @@ jobs: # uses: haraka/.github/.github/workflows/coverage.yml@master # secrets: inherit - test: - needs: [ lint, get-lts ] - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ ubuntu-latest, windows-latest ] - node-version: ${{ fromJson(needs.get-lts.outputs.active) }} - 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 - get-lts: - runs-on: ubuntu-latest - steps: - - id: get - uses: msimerson/node-lts-versions@v1 - outputs: - active: ${{ steps.get.outputs.active }} - lts: ${{ steps.get.outputs.lts }} + windows: + 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/.release b/.release index 0890e94..0bf2a09 160000 --- a/.release +++ b/.release @@ -1 +1 @@ -Subproject commit 0890e945e4e061c96c7b2ab45017525904c17728 +Subproject commit 0bf2a098d4792848c2103dfce0f911e00a14709e diff --git a/Changes.md b/CHANGELOG.md similarity index 78% rename from Changes.md rename to CHANGELOG.md index 268500a..8dce1ea 100644 --- a/Changes.md +++ b/CHANGELOG.md @@ -1,60 +1,62 @@ +# Changelog + +The format is based on [Keep a Changelog](https://keepachangelog.com/). + ### Unreleased +### [1.3.2] - 2025-01-26 + +- prefix node built-in modules with node: +- doc: mv Changes CHANGELOG.md +- doc(CONTRIBUTORS): added +- populate [files] in package.json. +- style: code formatting with prettier +- dep(eslint): upgrade to v9 ### [1.3.1] - 2023-03-02 - fix for loop returns (#35) - ### [1.3.0] - 2023-02-23 - add: defer options, similar to spamassassin.js #32 - es6: replace Object.keys().forEach with for...of - fix: wrap milter header adds in try/catch, fixes #28 - ### [1.2.0] - 2022-10-14 - Make milter header handling compatible with rspamd 3.3 (#30) - ### [1.1.8] - 2022-06-06 - doc(README): update CI badge URLs - ### 1.1.7 - 2022-06-05 - ci: replace travis & appveyor with GitHub actions - test: replace nodeunit with mocha - test: update header checks against lower cased header names - ### 1.1.6 - 2020-02-29 - Allow connecting to rspamd via unix sockets - ### 1.1.5 - 2019-04-01 - store symbols in results (for other plugins to inspect) - ### 1.1.4 - 2019-01-28 - fixed "TypeError: value.replace is not a function" - ### 1.1.3 - 2018-12-19 - add check.relay option - ### 1.1.2 - 2018-11-03 - add check.local_ip config option - ### 1.1.1 - 2018-05-10 - pass TLS-Cipher and TLS-Version headers to rspamd (fixes #4) @@ -62,21 +64,20 @@ - es6: use arrow functions - refactored hook_data_post, addressing excessive cognitive complexity - ### 1.1.0 - 2018-01-12 - use /checkv2 endpoint (requires rspamd 1.6+) - support setting SMTP message from rspamd - support 'rewrite subject' action - ### 1.0.0 - 2017-09-11 - initial release - [1.1.8]: https://github.com/haraka/haraka-plugin-rspamd/releases/tag/1.1.8 [1.1.9]: https://github.com/haraka/haraka-plugin-rspamd/releases/tag/1.1.9 [1.2.0]: https://github.com/haraka/haraka-plugin-rspamd/releases/tag/1.2.0 [1.3.0]: https://github.com/haraka/haraka-plugin-rspamd/releases/tag/1.3.0 [1.3.1]: https://github.com/haraka/haraka-plugin-rspamd/releases/tag/1.3.1 +[1.3.2]: https://github.com/haraka/haraka-plugin-rspamd/releases/tag/v1.3.2 +[1.1.6]: https://github.com/haraka/haraka-plugin-rspamd/releases/tag/v1.1.6 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 0000000..fa38a1f --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,10 @@ +# Contributors + +This handcrafted artisinal software is brought to you by: + +|
msimerson (17) |
fatalbanana (1) |
lobovkin (1) |
PHPGangsta (1) |
pjeby (1) |
analogic (1) |
lnedry (1) | +| :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +|
nishils (1) |
pvagner (1) | + +this file is generated by [.release](https://github.com/msimerson/.release). +Contribute to this project to get your GitHub profile included here. diff --git a/README.md b/README.md index fc5992c..7a954a6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ [![Build Status][ci-img]][ci-url] [![Code Climate][clim-img]][clim-url] -[![NPM][npm-img]][npm-url] # haraka-plugin-rspamd @@ -12,169 +11,167 @@ rspamd.ini - host - Default: localhost + Default: localhost - Host to connect to to query Rspamd. + Host to connect to to query Rspamd. - port - Default: 11333 + Default: 11333 - Port Rspamd is listening on. + Port Rspamd is listening on. - unix_socket - Path to a unix socket to connect to. If set, overrides host and port. + Path to a unix socket to connect to. If set, overrides host and port. -- add\_headers +- add_headers - Default: sometimes + Default: sometimes - Possible values are: + Possible values are: "always" - always add headers "never" - never add headers (unless provided by rspamd - see rmilter_headers) "sometimes" - add headers when rspamd recommends `add header` action - Format of these headers is governed by header.* settings + Format of these headers is governed by header.\* settings - reject.message - Default: Detected as spam + Default: Detected as spam - Message to send when rejecting mail due to Rspamd policy recommendation. + Message to send when rejecting mail due to Rspamd policy recommendation. - reject.spam - Default: true + Default: true - If set to false, ignore recommended *reject* action from Rspamd (except - for authenticated users). + If set to false, ignore recommended _reject_ action from Rspamd (except + for authenticated users). - reject.authenticated - Default: false + Default: false - Reject messages from authenticated users if Rspamd recommends *reject*. + Reject messages from authenticated users if Rspamd recommends _reject_. - check.authenticated - Default: false + Default: false - If true, messages from authenticated users will be scanned by Rspamd. + If true, messages from authenticated users will be scanned by Rspamd. - check.relay - Default: false + Default: false - If true, messages from relay clients will be scanned by Rspamd. + If true, messages from relay clients will be scanned by Rspamd. -- check.private\_ip +- check.private_ip - Default: false + Default: false - If false, messages from private IPs will not be scanned by Rspamd. - If true, messages from private IPs will be scanned by Rspamd. + If false, messages from private IPs will not be scanned by Rspamd. + If true, messages from private IPs will be scanned by Rspamd. -- check.local\_ip +- check.local_ip - Default: false + Default: false - If false, messages from localhost will not be scanned by Rspamd. - If true, messages from localhost will be scanned by Rspamd. + If false, messages from localhost will not be scanned by Rspamd. + If true, messages from localhost will be scanned by Rspamd. - dkim.enabled - Default: true + Default: true - If set to true, allow rspamd to add DKIM signatures to messages. + If set to true, allow rspamd to add DKIM signatures to messages. - header.bar - Default: undefined + Default: undefined - If set, add a visual spam level in a header with this name. + If set, add a visual spam level in a header with this name. - header.report - Default: undefined + Default: undefined - If set, add information about symbols matched & their scores in a header - with this name. + If set, add information about symbols matched & their scores in a header + with this name. - header.score - Default: undefined + Default: undefined - If set, add the numeric spam score in a header with this name. + If set, add the numeric spam score in a header with this name. -- rewrite\_subject.enabled +- rewrite_subject.enabled - Default: true + Default: true - If set to true, "rewrite subject" action is honored. + If set to true, "rewrite subject" action is honored. -- rmilter\_headers.enabled +- rmilter_headers.enabled - Default: true + Default: true - If set to true, allow rspamd to add/remove headers to messages via [task:set_milter_reply()](https://rspamd.com/doc/lua/task.html#m70081). + If set to true, allow rspamd to add/remove headers to messages via [task:set_milter_reply()](https://rspamd.com/doc/lua/task.html#m70081). -- smtp\_message.enabled +- smtp_message.enabled - Default: true + Default: true - If set to true, "smtp_message" provided by Rspamd is used in response for "reject" & "soft reject" actions. + If set to true, "smtp_message" provided by Rspamd is used in response for "reject" & "soft reject" actions. -- soft\_reject.enabled +- soft_reject.enabled - Default: true + Default: true - If set to true, allow rspamd to defer messages. + If set to true, allow rspamd to defer messages. -- soft\_reject.message +- soft_reject.message - Default: Deferred by policy + Default: Deferred by policy - Message to send to remote server on rspamd soft rejection. + Message to send to remote server on rspamd soft rejection. - spambar.positive - Default: + + Default: + - Used as character for visual spam-level where score is positive. + Used as character for visual spam-level where score is positive. - spambar.negative - Default: - + Default: - - Used as character for visual spam-level where score is negative. + Used as character for visual spam-level where score is negative. - spambar.neutral - Default: / + Default: / - Used as character for visual spam-level where score is zero. + Used as character for visual spam-level where score is zero. - subject - Default: [SPAM] %s + Default: [SPAM] %s - Subject to use for `rewrite subject` action if Rspamd does not provide one. + Subject to use for `rewrite subject` action if Rspamd does not provide one. - timeout (in seconds) - Default: 29 seconds - - How long to wait for a response from rspamd. + Default: 29 seconds + How long to wait for a response from rspamd. + [ci-img]: https://github.com/haraka/haraka-plugin-rspamd/actions/workflows/ci.yml/badge.svg [ci-url]: https://github.com/haraka/haraka-plugin-rspamd/actions/workflows/ci.yml [cov-img]: https://codecov.io/github/haraka/haraka-plugin-rspamd/coverage.svg [cov-url]: https://codecov.io/github/haraka/haraka-plugin-rspamd [clim-img]: https://codeclimate.com/github/haraka/haraka-plugin-rspamd/badges/gpa.svg [clim-url]: https://codeclimate.com/github/haraka/haraka-plugin-rspamd -[npm-img]: https://nodei.co/npm/haraka-plugin-rspamd.png -[npm-url]: https://www.npmjs.com/package/haraka-plugin-rspamd diff --git a/eslint.config.mjs b/eslint.config.mjs index 4634e99..d73beb7 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,39 +1,36 @@ -import haraka from "eslint-plugin-haraka"; -import globals from "globals"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import js from "@eslint/js"; -import { FlatCompat } from "@eslint/eslintrc"; +import globals from 'globals' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import js from '@eslint/js' +import { FlatCompat } from '@eslint/eslintrc' -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) const compat = new FlatCompat({ - baseDirectory: __dirname, - recommendedConfig: js.configs.recommended, - allConfig: js.configs.all -}); - -export default [...compat.extends("eslint:recommended", "plugin:haraka/recommended"), { - plugins: { - haraka, - }, + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}) +export default [ + ...compat.extends('@haraka'), + { languageOptions: { - globals: { - ...globals.node, - ...globals.mocha, - OK: true, - CONT: true, - DENY: true, - DENYSOFT: true, - DENYDISCONNECT: true, - DENYSOFTDISCONNECT: true, - }, + globals: { + ...globals.node, + ...globals.mocha, + }, }, rules: { - indent: ["error", 2, { - SwitchCase: 1, - }], + 'no-unused-vars': ['warn'], + indent: [ + 'error', + 2, + { + SwitchCase: 1, + }, + ], }, -}]; \ No newline at end of file + }, +] diff --git a/index.js b/index.js index 5f0ffcb..a4413b3 100644 --- a/index.js +++ b/index.js @@ -1,468 +1,468 @@ -'use strict'; +'use strict' // node built-ins -const http = require('http'); +const http = require('node:http') // haraka libs -const DSN = require('haraka-dsn'); +const DSN = require('haraka-dsn') exports.register = function () { - this.load_rspamd_ini(); + this.load_rspamd_ini() } exports.load_rspamd_ini = function () { - const plugin = this; - - plugin.cfg = plugin.config.get('rspamd.ini', { - booleans: [ - '-check.authenticated', - '+dkim.enabled', - '-check.private_ip', - '-check.local_ip', - '-check.relay', - '+reject.spam', - '-reject.authenticated', - '+rewrite_subject.enabled', - '+rmilter_headers.enabled', - '+soft_reject.enabled', - '+smtp_message.enabled', - '-defer.error', - '-defer.timeout', - ], - }, () => { - plugin.load_rspamd_ini(); - }); + const plugin = this + + plugin.cfg = plugin.config.get( + 'rspamd.ini', + { + booleans: [ + '-check.authenticated', + '+dkim.enabled', + '-check.private_ip', + '-check.local_ip', + '-check.relay', + '+reject.spam', + '-reject.authenticated', + '+rewrite_subject.enabled', + '+rmilter_headers.enabled', + '+soft_reject.enabled', + '+smtp_message.enabled', + '-defer.error', + '-defer.timeout', + ], + }, + () => { + plugin.load_rspamd_ini() + }, + ) if (!this.cfg.reject.message) { - this.cfg.reject.message = 'Detected as spam'; + this.cfg.reject.message = 'Detected as spam' } if (!this.cfg.soft_reject.message) { - this.cfg.soft_reject.message = 'Deferred by policy'; + this.cfg.soft_reject.message = 'Deferred by policy' } if (!this.cfg.spambar) { - this.cfg.spambar = { positive: '+', negative: '-', neutral: '/' }; + this.cfg.spambar = { positive: '+', negative: '-', neutral: '/' } } - if (!this.cfg.main.port) this.cfg.main.port = 11333; - if (!this.cfg.main.host) this.cfg.main.host = 'localhost'; + if (!this.cfg.main.port) this.cfg.main.port = 11333 + if (!this.cfg.main.host) this.cfg.main.host = 'localhost' if (!this.cfg.main.add_headers) { if (this.cfg.main.always_add_headers === true) { - this.cfg.main.add_headers = 'always'; - } - else { - this.cfg.main.add_headers = 'sometimes'; + this.cfg.main.add_headers = 'always' + } else { + this.cfg.main.add_headers = 'sometimes' } } if (!this.cfg.subject) { - this.cfg.subject = "[SPAM] %s"; + this.cfg.subject = '[SPAM] %s' } } exports.get_options = function (connection) { - // https://rspamd.com/doc/architecture/protocol.html // https://github.com/vstakhov/rspamd/blob/master/rules/http_headers.lua const options = { headers: {}, path: '/checkv2', method: 'POST', - }; + } if (this.cfg.main.unix_socket) { - options.socketPath = this.cfg.main.unix_socket; - } - else { - options.port = this.cfg.main.port; - options.host = this.cfg.main.host; + options.socketPath = this.cfg.main.unix_socket + } else { + options.port = this.cfg.main.port + options.host = this.cfg.main.host } if (connection.notes.auth_user) { - options.headers.User = connection.notes.auth_user; + options.headers.User = connection.notes.auth_user } - if (connection.remote.ip) options.headers.IP = connection.remote.ip; + if (connection.remote.ip) options.headers.IP = connection.remote.ip - const fcrdns = connection.results.get('fcrdns'); + const fcrdns = connection.results.get('fcrdns') if (fcrdns && fcrdns.fcrdns && fcrdns.fcrdns[0]) { - options.headers.Hostname = fcrdns.fcrdns[0]; - } - else { + options.headers.Hostname = fcrdns.fcrdns[0] + } else { if (connection.remote.host) { - options.headers.Hostname = connection.remote.host; + options.headers.Hostname = connection.remote.host } } - if (connection.hello.host) options.headers.Helo = connection.hello.host; + if (connection.hello.host) options.headers.Helo = connection.hello.host - let spf = connection.transaction.results.get('spf'); + let spf = connection.transaction.results.get('spf') if (spf && spf.result) { - options.headers.SPF = { result: spf.result.toLowerCase() }; - } - else { - spf = connection.results.get('spf'); + options.headers.SPF = { result: spf.result.toLowerCase() } + } else { + spf = connection.results.get('spf') if (spf && spf.result) { - options.headers.SPF = { result: spf.result.toLowerCase() }; + options.headers.SPF = { result: spf.result.toLowerCase() } } } if (connection.transaction.mail_from) { - const mfaddr = connection.transaction.mail_from.address().toString(); + const mfaddr = connection.transaction.mail_from.address().toString() if (mfaddr) { - options.headers.From = mfaddr; + options.headers.From = mfaddr } } - const rcpts = connection.transaction.rcpt_to; + const rcpts = connection.transaction.rcpt_to if (rcpts) { - options.headers.Rcpt = []; + options.headers.Rcpt = [] for (const rcpt of rcpts) { - options.headers.Rcpt.push(rcpt.address()); + options.headers.Rcpt.push(rcpt.address()) } // for per-user options if (rcpts.length === 1) { - options.headers['Deliver-To'] = options.headers.Rcpt[0]; + options.headers['Deliver-To'] = options.headers.Rcpt[0] } } if (connection.transaction.uuid) - options.headers['Queue-Id'] = connection.transaction.uuid; + options.headers['Queue-Id'] = connection.transaction.uuid if (connection.tls.enabled) { - options.headers['TLS-Cipher'] = connection.tls.cipher.name; - options.headers['TLS-Version'] = connection.tls.cipher.version; + options.headers['TLS-Cipher'] = connection.tls.cipher.name + options.headers['TLS-Version'] = connection.tls.cipher.version } - return options; + return options } exports.get_smtp_message = function (r) { + if (!this.cfg.smtp_message.enabled || !r.data.messages) return + if (typeof r.data.messages !== 'object') return + if (!r.data.messages.smtp_message) return - if (!this.cfg.smtp_message.enabled || !r.data.messages) return; - if (typeof(r.data.messages) !== 'object') return; - if (!r.data.messages.smtp_message) return; - - return r.data.messages.smtp_message; + return r.data.messages.smtp_message } exports.do_rewrite = function (connection, data) { + if (!this.cfg.rewrite_subject.enabled) return false + if (data.action !== 'rewrite subject') return false - if (!this.cfg.rewrite_subject.enabled) return false; - if (data.action !== 'rewrite subject') return false; - - const rspamd_subject = data.subject || this.cfg.subject; - const old_subject = connection.transaction.header.get('Subject') || ''; - const new_subject = rspamd_subject.replace('%s', old_subject); + const rspamd_subject = data.subject || this.cfg.subject + const old_subject = connection.transaction.header.get('Subject') || '' + const new_subject = rspamd_subject.replace('%s', old_subject) - connection.transaction.remove_header('Subject'); - connection.transaction.add_header('Subject', new_subject); + connection.transaction.remove_header('Subject') + connection.transaction.add_header('Subject', new_subject) } exports.add_dkim_header = function (connection, data) { + if (!this.cfg.dkim.enabled) return + if (!data['dkim-signature']) return - if (!this.cfg.dkim.enabled) return; - if (!data['dkim-signature']) return; - - connection.transaction.add_header('DKIM-Signature', data['dkim-signature']); + connection.transaction.add_header('DKIM-Signature', data['dkim-signature']) } exports.do_milter_headers = function (connection, data) { - - if (!this.cfg.rmilter_headers.enabled) return; - if (!data.milter) return; + if (!this.cfg.rmilter_headers.enabled) return + if (!data.milter) return if (data.milter.remove_headers) { for (const key of Object.keys(data.milter.remove_headers)) { - connection.transaction.remove_header(key); + connection.transaction.remove_header(key) } } if (data.milter.add_headers) { try { - connection.logdebug(this, `milter.add_headers: ${JSON.stringify(data.milter.add_headers)}`); + connection.logdebug( + this, + `milter.add_headers: ${JSON.stringify(data.milter.add_headers)}`, + ) for (const key of Object.keys(data.milter.add_headers)) { - const header_values = data.milter.add_headers[key]; - if (!header_values) continue; + const header_values = data.milter.add_headers[key] + if (!header_values) continue if (Object.prototype.toString.call(header_values) == '[object Array]') { header_values.forEach(function (header_value, header_index) { if (typeof header_value === 'object') { - connection.transaction.add_header(key, header_value.value); + connection.transaction.add_header(key, header_value.value) + } else { + connection.transaction.add_header(key, header_value) } - else { - connection.transaction.add_header(key, header_value); - } - }); - } - else if (typeof header_values === 'object') { - connection.transaction.add_header(key, header_values.value); - } - else { - connection.transaction.add_header(key, header_values); + }) + } else if (typeof header_values === 'object') { + connection.transaction.add_header(key, header_values.value) + } else { + connection.transaction.add_header(key, header_values) } } - } - catch (err) { + } catch (err) { connection.errorlog(this, `milter.addheaders error: ${err}`) } } } exports.hook_data_post = function (next, connection) { - const plugin = this; + const plugin = this - if (!connection.transaction) return next(); - if (!plugin.should_check(connection)) return next(); + if (!connection.transaction) return next() + if (!plugin.should_check(connection)) return next() - let timer; - const timeout = plugin.cfg.main.timeout || plugin.timeout - 1; + let timer + const timeout = plugin.cfg.main.timeout || plugin.timeout - 1 - let calledNext=false; - function nextOnce (code, msg) { - clearTimeout(timer); - if (calledNext) return; - calledNext=true; - if (!connection?.transaction) return; - next(code, msg); + let calledNext = false + function nextOnce(code, msg) { + clearTimeout(timer) + if (calledNext) return + calledNext = true + if (!connection?.transaction) return + next(code, msg) } timer = setTimeout(() => { - if (!connection?.transaction) return; - connection.transaction.results.add(plugin, {err: 'timeout'}); - if (plugin.cfg.defer.timeout) return nextOnce(DENYSOFT, 'Rspamd scan timeout'); - nextOnce(); - }, timeout * 1000); + if (!connection?.transaction) return + connection.transaction.results.add(plugin, { err: 'timeout' }) + if (plugin.cfg.defer.timeout) + return nextOnce(DENYSOFT, 'Rspamd scan timeout') + nextOnce() + }, timeout * 1000) - const start = Date.now(); + const start = Date.now() const req = http.request(plugin.get_options(connection), (res) => { - let rawData = ''; + let rawData = '' - res.on('data', (chunk) => { rawData += chunk; }); + res.on('data', (chunk) => { + rawData += chunk + }) res.on('end', () => { - if (!connection.transaction) return nextOnce(); //client gone + if (!connection.transaction) return nextOnce() //client gone - const r = plugin.parse_response(rawData, connection); + const r = plugin.parse_response(rawData, connection) if (!r || !r.data || !r.log) { - if (plugin.cfg.defer.error) return nextOnce(DENYSOFT, 'Rspamd scan error'); - return nextOnce(); + if (plugin.cfg.defer.error) + return nextOnce(DENYSOFT, 'Rspamd scan error') + return nextOnce() } - r.log.emit = true; // spit out a log entry - r.log.time = (Date.now() - start)/1000; + r.log.emit = true // spit out a log entry + r.log.time = (Date.now() - start) / 1000 - connection.transaction.results.add(plugin, r.log); - if (r.data.symbols) connection.transaction.results.add(plugin, { symbols: r.data.symbols }); + connection.transaction.results.add(plugin, r.log) + if (r.data.symbols) + connection.transaction.results.add(plugin, { symbols: r.data.symbols }) - const smtp_message = plugin.get_smtp_message(r); + const smtp_message = plugin.get_smtp_message(r) - plugin.do_rewrite(connection, r.data); + plugin.do_rewrite(connection, r.data) if (plugin.cfg.soft_reject.enabled && r.data.action === 'soft reject') { - nextOnce(DENYSOFT, DSN.sec_unauthorized(smtp_message || plugin.cfg.soft_reject.message, 451)); + nextOnce( + DENYSOFT, + DSN.sec_unauthorized( + smtp_message || plugin.cfg.soft_reject.message, + 451, + ), + ) + } else if (plugin.wants_reject(connection, r.data)) { + nextOnce(DENY, smtp_message || plugin.cfg.reject.message) + } else { + plugin.add_dkim_header(connection, r.data) + plugin.do_milter_headers(connection, r.data) + plugin.add_headers(connection, r.data) + + nextOnce() } - else if (plugin.wants_reject(connection, r.data)) { - nextOnce(DENY, smtp_message || plugin.cfg.reject.message); - } - else { - plugin.add_dkim_header(connection, r.data); - plugin.do_milter_headers(connection, r.data); - plugin.add_headers(connection, r.data); - - nextOnce(); - } - }); + }) }) req.on('error', (err) => { - if (!connection?.transaction) return nextOnce(); // client gone - connection.transaction.results.add(plugin, { err: err.message}); - if (plugin.cfg.defer.error) return nextOnce(DENYSOFT, 'Rspamd scan error'); - nextOnce(); - }); + if (!connection?.transaction) return nextOnce() // client gone + connection.transaction.results.add(plugin, { err: err.message }) + if (plugin.cfg.defer.error) return nextOnce(DENYSOFT, 'Rspamd scan error') + nextOnce() + }) - connection.transaction.message_stream.pipe(req); + connection.transaction.message_stream.pipe(req) // pipe calls req.end() asynchronously } exports.should_check = function (connection) { - - let result = true; // default + let result = true // default if (this.cfg.check.authenticated == false && connection.notes.auth_user) { - connection.transaction.results.add(this, { skip: 'authed'}); - result = false; + connection.transaction.results.add(this, { skip: 'authed' }) + result = false } if (this.cfg.check.relay == false && connection.relaying) { - connection.transaction.results.add(this, { skip: 'relay'}); - result = false; + connection.transaction.results.add(this, { skip: 'relay' }) + result = false } if (this.cfg.check.local_ip == false && connection.remote.is_local) { - connection.transaction.results.add(this, { skip: 'local_ip'}); - result = false; + connection.transaction.results.add(this, { skip: 'local_ip' }) + result = false } if (this.cfg.check.private_ip == false && connection.remote.is_private) { if (this.cfg.check.local_ip == true && connection.remote.is_local) { // local IPs are included in private IPs - } - else { - connection.transaction.results.add(this, { skip: 'private_ip'}); - result = false; + } else { + connection.transaction.results.add(this, { skip: 'private_ip' }) + result = false } } - return result; + return result } exports.wants_reject = function (connection, data) { - - if (data.action !== 'reject') return false; + if (data.action !== 'reject') return false if (connection.notes.auth_user) { - if (this.cfg.reject.authenticated == false) return false; - } - else { - if (this.cfg.reject.spam == false) return false; + if (this.cfg.reject.authenticated == false) return false + } else { + if (this.cfg.reject.spam == false) return false } - return true; + return true } exports.wants_headers_added = function (rspamd_data) { - - if (this.cfg.main.add_headers === 'never') return false; - if (this.cfg.main.add_headers === 'always') return true; + if (this.cfg.main.add_headers === 'never') return false + if (this.cfg.main.add_headers === 'always') return true // implicit add_headers=sometimes, based on rspamd response - if (rspamd_data.action === 'add header') return true; - return false; + if (rspamd_data.action === 'add header') return true + return false } exports.get_clean = function (data, connection) { - const clean = { symbols: {} }; + const clean = { symbols: {} } if (data.symbols) { - Object.keys(data.symbols).forEach(key => { - const a = data.symbols[key]; + Object.keys(data.symbols).forEach((key) => { + const a = data.symbols[key] // transform { name: KEY, score: VAL } -> { KEY: VAL } if (a.name && a.score !== undefined) { - clean.symbols[ a.name ] = a.score; - return; + clean.symbols[a.name] = a.score + return } // unhandled type - connection.logerror(this, a); + connection.logerror(this, a) }) } // objects that may exist - const skip_keys = ['action', 'is_skipped', 'required_score', 'score']; + const skip_keys = ['action', 'is_skipped', 'required_score', 'score'] for (const key of skip_keys) { switch (typeof data[key]) { case 'boolean': case 'number': case 'string': - clean[key] = data[key]; - break; + clean[key] = data[key] + break default: - connection.loginfo(this, `skipping unhandled: ${typeof data[key]}`); + connection.loginfo(this, `skipping unhandled: ${typeof data[key]}`) } } // arrays which might be present - const arrays = ['urls', 'emails', 'messages']; + const arrays = ['urls', 'emails', 'messages'] for (const b of arrays) { // collapse to comma separated string, so values get logged - if (!data[b]) continue; + if (!data[b]) continue if (data[b].length) { - clean[b] = data[b].join(','); - continue; + clean[b] = data[b].join(',') + continue } - if (typeof(data[b]) == 'object') { + if (typeof data[b] == 'object') { // 'messages' is probably a dictionary - Object.keys(data[b]).map((k) => { - return `${k} : ${data[b][k]}`; - }).join(','); + Object.keys(data[b]) + .map((k) => { + return `${k} : ${data[b][k]}` + }) + .join(',') } } - return clean; + return clean } exports.parse_response = function (rawData, connection) { - if (!rawData) return; + if (!rawData) return - let data; + let data try { - data = JSON.parse(rawData); - } - catch (err) { + data = JSON.parse(rawData) + } catch (err) { connection.transaction.results.add(this, { - err: `parse failure: ${err.message}` - }); - return; + err: `parse failure: ${err.message}`, + }) + return } - if (Object.keys(data).length === 0) return; + if (Object.keys(data).length === 0) return if (Object.keys(data).length === 1 && data.error) { - connection.transaction.results.add(this, { err: data.error }); - return; + connection.transaction.results.add(this, { err: data.error }) + return } return { data, - 'log' : this.get_clean(data, connection), - }; + log: this.get_clean(data, connection), + } } exports.add_headers = function (connection, data) { - const cfg = this.cfg; + const cfg = this.cfg - if (!this.wants_headers_added(data)) return; + if (!this.wants_headers_added(data)) return if (cfg.header && cfg.header.bar) { - let spamBar = ''; - let spamBarScore = 1; - let spamBarChar = cfg.spambar.neutral || '/'; + let spamBar = '' + let spamBarScore = 1 + let spamBarChar = cfg.spambar.neutral || '/' if (data.score >= 1) { - spamBarScore = Math.floor(data.score); - spamBarChar = cfg.spambar.positive || '+'; - } - else if (data.score <= -1) { - spamBarScore = Math.floor(data.score * -1); - spamBarChar = cfg.spambar.negative || '-'; + spamBarScore = Math.floor(data.score) + spamBarChar = cfg.spambar.positive || '+' + } else if (data.score <= -1) { + spamBarScore = Math.floor(data.score * -1) + spamBarChar = cfg.spambar.negative || '-' } for (let i = 0; i < spamBarScore; i++) { - spamBar += spamBarChar; + spamBar += spamBarChar } - connection.transaction.remove_header(cfg.header.bar); - connection.transaction.add_header(cfg.header.bar, spamBar); + connection.transaction.remove_header(cfg.header.bar) + connection.transaction.add_header(cfg.header.bar, spamBar) } if (cfg.header && cfg.header.report) { - const prettySymbols = []; + const prettySymbols = [] for (const k in data.symbols) { if (data.symbols[k].score) { - prettySymbols.push(`${data.symbols[k].name}(${data.symbols[k].score})`); + prettySymbols.push(`${data.symbols[k].name}(${data.symbols[k].score})`) } } - connection.transaction.remove_header(cfg.header.report); - connection.transaction.add_header(cfg.header.report, - prettySymbols.join(' ')); + connection.transaction.remove_header(cfg.header.report) + connection.transaction.add_header( + cfg.header.report, + prettySymbols.join(' '), + ) } if (cfg.header && cfg.header.score) { - connection.transaction.remove_header(cfg.header.score); - connection.transaction.add_header(cfg.header.score, `${data.score}`); + connection.transaction.remove_header(cfg.header.score) + connection.transaction.add_header(cfg.header.score, `${data.score}`) } } diff --git a/package.json b/package.json index 6b03eab..2c0c7aa 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,17 @@ { "name": "haraka-plugin-rspamd", - "version": "1.3.1", + "version": "1.3.2", "description": "Haraka plugin for rspamd", "main": "index.js", "scripts": { + "format": "npm run prettier:fix && npm run lint:fix", "lint": "npx eslint *.js test", - "lintfix": "npx eslint --fix *.js test", - "test": "npx _mocha" + "lint:fix": "npx eslint --fix *.js test", + "prettier": "npx prettier . --check", + "prettier:fix": "npx prettier . --write --log-level=warn", + "test": "npx mocha@^11", + "versions": "npx dependency-version-checker check", + "versions:fix": "npx dependency-version-checker update" }, "repository": { "type": "git", @@ -24,12 +29,14 @@ }, "homepage": "https://github.com/haraka/haraka-plugin-rspamd#readme", "devDependencies": { - "eslint": ">=8", - "eslint-plugin-haraka": "*", - "haraka-test-fixtures": "^1.0.35", - "mocha": ">=9" + "haraka-test-fixtures": "^1.3.8", + "@haraka/eslint-config": "^2.0.2" }, "dependencies": { - "haraka-dsn": "*" - } + "haraka-dsn": "^1.1.0" + }, + "files": [ + "CHANGELOG.md", + "config" + ] } diff --git a/test/fixtures/spam.eml b/test/fixtures/spam.eml new file mode 100644 index 0000000..773132c --- /dev/null +++ b/test/fixtures/spam.eml @@ -0,0 +1,168 @@ +Return-Path: +Received: by mail-wm1-f69.google.com with SMTP id 5b1f17b1804b1-436328fcfeesf29271215e9.1 + for ; Sun, 26 Jan 2025 08:13:53 -0800 (PST) +ARC-Seal: i=2; a=rsa-sha256; t=1737907971; cv=pass; + d=google.com; s=arc-20240605; + b=VWihnGBn7J+aaG+uER1UU1NUgaSxihjBbA5WPpSkQ/TvobhfgekG5y2fwsHGRMB9At + c9DcrbLao9K1QnwMRU5D+rJOujyFQcNfWabhrW4HUUhJcFGhTFmV0rMyx8fz+QM0c0sj + ob3jcAj3BSGk1WkPBraL6+3L+Y0NOJnRx8xNuejSN0lYwFJaLR+JBOnFxoxVRpBkS2WA + vK7A+Auc2PwLhya6qYba3XbMTUwHa7YrIyMBJ2tzBIiG57F27Wp5ORcbTLeOtRK12+5s + rZxC2ptn7JjrWf+pDPWKQzMj3ItZ6E2/B/jwA81HrkwEDNWLkJAFNYl9Ug0rYvAjuWaw + nacQ== +ARC-Message-Signature: i=2; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20240605; + h=list-unsubscribe:list-subscribe:list-archive:list-help:list-post + :list-id:mailing-list:precedence:to:subject:message-id:date:from + :mime-version:dkim-signature; + bh=y5KsxE/KDsY78hvsBUXoS75hnCryj31gk1NrPrKBoJo=; + fh=wvdKgIywERlpanRU9k3IdtcKzFPglbCcaQi6ZF9pu3E=; + b=Dq2HXSTV5RVXanplx9EU3d/VBN2JpY9pA6FzYjgySE1hEyqTQEtM9esOyJhS3nlLHb + s4APIhqiW073W/AJWljhPfkdx70r1leSQ0ewVjMMdtRkDBOC/S6Q3LeOH8p63v/J7eNB + X8JBHB5l/21T6ik1zr5j0ul0Opd4BV6BuEF/y/4kZ2Y3+AJD5tagw6pvfe0SCOUG6hsY + Q+BD+dO/S5dyaZRYFTsQthkBlLs5Z1cuRL17hgjm1cmcGWsqtzdN5sNkP/unOWPaJk82 + qVJh7+WEDdFwEybeV2cYf2tbo6jE1z/mT4uJJhls74ICGpljAv4br4LwGcELWx2xj4u0 + NbLw==; + darn=redacted.net +ARC-Authentication-Results: i=2; mx.google.com; + dkim=pass header.i=@zaho.123win.dog header.s=google header.b=Bw8HiAqx; + spf=pass (google.com: domain of serviiiice@zaho.123win.dog designates 209.85.220.41 as permitted sender) smtp.mailfrom=serviiiice@zaho.123win.dog; + dara=pass header.i=@zaho.123win.dog +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; + d=zaho.123win.dog; s=google; t=1737907971; x=1738512771; darn=redacted.net; + h=list-unsubscribe:list-subscribe:list-archive:list-help:list-post + :list-id:mailing-list:precedence:x-original-authentication-results + :x-original-sender:to:subject:message-id:date:from:mime-version:from + :to:cc:subject:date:message-id:reply-to; + bh=y5KsxE/KDsY78hvsBUXoS75hnCryj31gk1NrPrKBoJo=; + b=Y2t16eHPMCDvglhpFZg/3m8vV8B+iZ72h1pnX+yKETgbwgwBSVWUwtTGcdNO/sSMG5 + 3bg8w59zjmgqf6AFjSqkLx37ePSkWvw9nWowaEMt+UpieatoQM1WhbVW7Yw0pH0kGT68 + 6RvdJhmrvfKqmm32+9yfAc432fVrXdlmYBELE= +X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; + d=1e100.net; s=20230601; t=1737907971; x=1738512771; + h=list-unsubscribe:list-subscribe:list-archive:list-help:list-post + :x-spam-checked-in-group:list-id:mailing-list:precedence + :x-original-authentication-results:x-original-sender:to:subject + :message-id:date:from:mime-version:x-beenthere:x-beenthere + :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to; + bh=y5KsxE/KDsY78hvsBUXoS75hnCryj31gk1NrPrKBoJo=; + b=fKqNxNBXRVvNmeWnhPDzgdkMCFEFshRjut7ZGcNUMs63Jlc8rJ81ytiDj/Gs6RMRnN + 80PxvWNOq3Y/WCi1hiNNpSNaKzEBjrMiUY8nWuR4ji91o4VACb4b0KkERfDlEDNx9RaF + qb2/FGft3STN9uUHqmBZe/RYjaMEDJm/TnSx2pRx9PSv0QvxkraNDCW+4ftRnU5fg79o + SsDNcP2h9cf9UgZlKx4JQ5JCP7CuFVW/56NM2QpQAifC4IKdQ8EB7t65Z2Oi+yTcpnhL + OsOHswwKgZmT/s9P4nw2+BBRtvvj0/9DslJmIRlNnrmRRruLm3un+Kwa/dfd7epzMGCB + 4Xww== +X-Forwarded-Encrypted: i=2; AJvYcCVkA2XtpygNIyISdA8WAMFTXis+wyTGQhFkWhHzs3EQVmlTOiAqBgidVKTgvbTcboE60uOp@redacted.net +X-Gm-Message-State: AOJu0YyyY6ypsRCWp0Qq6aIi7bfE2YaTEi9v1gGLKU6QA1kyltdqq+jz + ZDdB3+SFLrI8UosbjizhVgpDnw8fHwii2hWNmFxzhzEdVt/6nekk6CXkE3RZpBo= +X-Google-Smtp-Source: AGHT+IGwvjObaU2m1e53rJOCBLYlfeAOeNR4ugFNThASjMMATSMnXFogYiAB8km2K/TjIIAUs2Nwag== +X-Received: by 2002:a05:6512:124e:b0:540:2549:b5ad with SMTP id 2adb3069b0e04-5439c246d35mr12834661e87.22.1737907561264; + Sun, 26 Jan 2025 08:06:01 -0800 (PST) +X-BeenThere: ki@zaho.123win.dog +Received: by 2002:a05:651c:221d:b0:300:40ad:298f with SMTP id + 38308e7fff4ca-30761c5e064ls1292611fa.0.-pod-prod-02-eu; Sun, 26 Jan 2025 + 08:05:26 -0800 (PST) +X-Forwarded-Encrypted: i=2; AJvYcCXVj5B2hbdVer0sFsIPoZTFZEFpqBA8tHVQNBUAWsFsYtfgauPH7PYZlabJkmus2NCyyg==@zaho.123win.dog +X-Received: by 2002:a05:651c:10a4:b0:302:1fce:392e with SMTP id 38308e7fff4ca-3072cb12ef1mr121302341fa.25.1737907525449; + Sun, 26 Jan 2025 08:05:25 -0800 (PST) +X-BeenThere: stechniquueeee@zaho.123win.dog +Received: by 2002:a2e:a54f:0:b0:304:588a:99d3 with SMTP id 38308e7fff4ca-30761d773adls1695671fa.1.-pod-prod-02-eu; + Sun, 26 Jan 2025 08:05:23 -0800 (PST) +X-Received: by 2002:a05:6512:4849:b0:542:29b6:9c1a with SMTP id 2adb3069b0e04-5439c2877c7mr10950326e87.51.1737907523029; + Sun, 26 Jan 2025 08:05:23 -0800 (PST) +ARC-Seal: i=1; a=rsa-sha256; t=1737907523; cv=none; + d=google.com; s=arc-20240605; + b=JqtjbfDp7ci2XldkrwwU3Hk4W+LvWQ+VWj4lpJWiqbhQZVq5o7qHqyoYwsMmk9vmuW + 3tIzC/WNOH2fro/HH2aCLcy8Ba6/h+1QoluyTmgVR+TxNmK1skfONda8gFu5/NwU9xxu + Knbw15tbiiJ26YZRNoIWPAzPdyNrdVBQGzloPBLt4IpgOM85pjKLnLnM/UJ6D5lcf7c7 + PRZFchSNTOq4gYuSY9jYrP07eIZXzVZPPMFTInvK/VZgqQopLDz8xzAqR9rOSHNMDPn1 + ehlcqceYhmUWJCQnUAs9JMXj1hK+g6PdzHOpnnWX93TJXodpdp6Go8k11Aa7ekMc/CBi + O+bQ== +ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20240605; + h=to:subject:message-id:date:from:mime-version:dkim-signature; + bh=y5KsxE/KDsY78hvsBUXoS75hnCryj31gk1NrPrKBoJo=; + fh=+DjsBAu3vMy32nUDMWtUNFAWWFItuDlhjOwOdXOg8cg=; + b=KCiT7ZRBm3UTCeM5an+cWVXyNwRMPfgFLLDjcY++zcSWlzd3LCCHnNFCnsmRy0uTPS + dVpPQ3++6piWa1dxhOVShXshI6WQTC+ycsujG3oct8RslGKpPEx6DwhLPUTwXhNIFDjv + gUNKn6yampcYyZI5tOyWHCAZ9viXvGTkUeaYaG94uZzlz1t3QQYGCleaHydHIhtK/EXL + eBsP255RFgIB4boFypZh20zp0/PAu55BObKp8VlfsNEMWgU5QS62HDRAikdVlvALVHTL + A2oD3nj7bYXS5IdXpX316sijqm0+p8hFZdRH4N+eqT5bcrw9d0JhnOj9PWntHTjeYmGk + XMAg==; + dara=google.com +ARC-Authentication-Results: i=1; mx.google.com; + dkim=pass header.i=@zaho.123win.dog header.s=google header.b=Bw8HiAqx; + spf=pass (google.com: domain of serviiiice@zaho.123win.dog designates 209.85.220.41 as permitted sender) smtp.mailfrom=serviiiice@zaho.123win.dog; + dara=pass header.i=@zaho.123win.dog +Received: from mail-sor-f41.google.com (mail-sor-f41.google.com. [209.85.220.41]) + by mx.google.com with SMTPS id 38308e7fff4ca-3076ba4f00esor10968021fa.1.2025.01.26.08.05.22 + for + (Google Transport Security); + Sun, 26 Jan 2025 08:05:22 -0800 (PST) +Received-SPF: pass (google.com: domain of serviiiice@zaho.123win.dog designates 209.85.220.41 as permitted sender) client-ip=209.85.220.41; +X-Gm-Gg: ASbGncshXES2dk01NITjT1Q1CAAJ0neEGe1FAVip8W+N4G0QAQ0vurfCDRuf82O5jri + fylcxg+qBW34hzAU2N4iMRrYdDsrwEaptz6kM2VkyLgC12QtQux5Pic3DnLF4sQ== +X-Received: by 2002:a2e:b555:0:b0:302:22e6:5f8 with SMTP id + 38308e7fff4ca-3072ca9a6f7mr108778841fa.22.1737907522169; Sun, 26 Jan 2025 + 08:05:22 -0800 (PST) +MIME-Version: 1.0 +From: Repair your teeth +Date: Sun, 26 Jan 2025 08:05:11 -0800 +X-Gm-Features: AWEUYZlRnFchucj3BSMH8GD7_jPNFGYW316YoHEeaVYWa1CCCoQQbmKjGhnSd5M +Message-ID: +Subject: Swish This Sour Liquid In Your Mouth To Regrow Teeth And Gums Overnight +To: stechniquueeee@zaho.123win.dog +Content-Type: multipart/alternative; boundary="000000000000830b18062c9e2007" +X-Original-Sender: serviiiice@zaho.123win.dog +X-Original-Authentication-Results: mx.google.com; dkim=pass + header.i=@zaho.123win.dog header.s=google header.b=Bw8HiAqx; spf=pass + (google.com: domain of serviiiice@zaho.123win.dog designates 209.85.220.41 as + permitted sender) smtp.mailfrom=serviiiice@zaho.123win.dog; dara=pass header.i=@zaho.123win.dog +Precedence: list +Mailing-list: list ki@zaho.123win.dog; contact ki+owners@zaho.123win.dog +List-ID: +X-Spam-Checked-In-Group: stechniquueeee@zaho.123win.dog +X-Google-Group-Id: 120555917144 +List-Post: , +List-Help: , + +List-Archive: +List-Subscribe: , + +List-Unsubscribe: , + + +--000000000000830b18062c9e2007 +Content-Type: text/plain; charset="UTF-8" +Content-Transfer-Encoding: quoted-printable + +Do you touch your gums like this with your tongue? + +STOP RIGHT NOW or risk losing all your teeth! + +Recent studies have shown that people who touch their gums with their +tongue this way have 90% more rotten teeth compared to those who don=E2=80= +=99t. + +*Click here * to find why this habit +might cause all your teeth to fall out and what you can do about it. + + + +--000000000000830b18062c9e2007 +Content-Type: text/html; charset="UTF-8" +Content-Transfer-Encoding: quoted-printable + +
Do you touch your gums like this with your tongue?

= +STOP RIGHT NOW or risk losing all your teeth!

Recent studies have sh= +own that people who touch their gums with their tongue this way have 90% mo= +re rotten teeth compared to those who don=E2=80=99t.

Click here to find why this habit might cause all your teeth to fall out and wha= +t you can do about it.







+ +--000000000000830b18062c9e2007-- + diff --git a/test/index.js b/test/index.js index 74d996d..72bcbad 100644 --- a/test/index.js +++ b/test/index.js @@ -1,46 +1,44 @@ -'use strict'; +'use strict' -const assert = require('assert') +const assert = require('node:assert') +const fs = require('node:fs') -const fixtures = require('haraka-test-fixtures'); -const connection = fixtures.connection; +const fixtures = require('haraka-test-fixtures') +const connection = fixtures.connection -function _set_up (done) { - this.plugin = new fixtures.plugin('rspamd'); - this.plugin.register(); - this.connection = connection.createConnection(); - this.connection.transaction = fixtures.transaction.createTransaction() - // this.connection.init_transaction(); +function _set_up(done) { + this.plugin = new fixtures.plugin('rspamd') + this.plugin.register() + this.connection = connection.createConnection() + this.connection.init_transaction(); - done(); + done() } describe('register', function () { - beforeEach(_set_up) it('loads the rspamd plugin', function (done) { - assert.equal('rspamd', this.plugin.name); - done(); + assert.equal('rspamd', this.plugin.name) + done() }) it('register loads rspamd.ini', function (done) { - this.plugin.register(); - assert.ok(this.plugin.cfg); - assert.equal(true, this.plugin.cfg.reject.spam); - assert.ok(this.plugin.cfg.header.bar); - done(); + this.plugin.register() + assert.ok(this.plugin.cfg) + assert.equal(true, this.plugin.cfg.reject.spam) + assert.ok(this.plugin.cfg.header.bar) + done() }) }) describe('add_headers', function () { - beforeEach(_set_up) it('add_headers exists as function', function (done) { // console.log(this.plugin.cfg); - assert.equal('function', typeof this.plugin.add_headers); - done(); + assert.equal('function', typeof this.plugin.add_headers) + done() }) it('adds a header to a message with positive score', function (done) { @@ -49,7 +47,7 @@ describe('add_headers', function () { symbols: { FOO: { name: 'FOO', - score: 0.100000, + score: 0.1, description: 'foo', options: ['foo', 'bar'], }, @@ -57,64 +55,74 @@ describe('add_headers', function () { name: 'BAR', score: 1.0, description: 'bar', - } - } - }; - this.plugin.cfg.main.add_headers = 'always'; - this.plugin.add_headers(this.connection, test_data); - assert.deepEqual(this.connection.transaction.header.headers['x-rspamd-score'], [ '1.1' ]); - assert.deepEqual(this.connection.transaction.header.headers['x-rspamd-bar'], ['+']); - assert.deepEqual(this.connection.transaction.header.headers['x-rspamd-report'], ['FOO(0.1) BAR(1)']); - done(); + }, + }, + } + this.plugin.cfg.main.add_headers = 'always' + this.plugin.add_headers(this.connection, test_data) + assert.deepEqual( + this.connection.transaction.header.headers['x-rspamd-score'], + ['1.1'], + ) + assert.deepEqual( + this.connection.transaction.header.headers['x-rspamd-bar'], + ['+'], + ) + assert.deepEqual( + this.connection.transaction.header.headers['x-rspamd-report'], + ['FOO(0.1) BAR(1)'], + ) + done() }) it('adds a header to a message with negative score', function (done) { const test_data = { - score: -1 - }; - this.plugin.cfg.main.add_headers = 'always'; - this.plugin.add_headers(this.connection, test_data); + score: -1, + } + this.plugin.cfg.main.add_headers = 'always' + this.plugin.add_headers(this.connection, test_data) // console.log(this.connection.transaction.header); - assert.deepEqual(this.connection.transaction.header.headers['x-rspamd-score'], ['-1']); - assert.deepEqual(this.connection.transaction.header.headers['x-rspamd-bar'], ['-']); - done(); + assert.deepEqual( + this.connection.transaction.header.headers['x-rspamd-score'], + ['-1'], + ) + assert.deepEqual( + this.connection.transaction.header.headers['x-rspamd-bar'], + ['-'], + ) + done() }) }) - describe('wants_headers_added', function () { - beforeEach(_set_up) it('wants no headers when add_headers=never', function (done) { - this.plugin.cfg.main.add_headers='never'; + this.plugin.cfg.main.add_headers = 'never' assert.equal( this.plugin.wants_headers_added({ action: 'add header' }), - false - ); - done(); + false, + ) + done() }) it('always wants no headers when add_headers=always', function (done) { - this.plugin.cfg.main.add_headers='always'; - assert.equal( - this.plugin.wants_headers_added({ action: 'beat it' }), - true - ); - done(); + this.plugin.cfg.main.add_headers = 'always' + assert.equal(this.plugin.wants_headers_added({ action: 'beat it' }), true) + done() }) it('wants headers when rspamd response indicates, add_headers=sometimes', function (done) { - this.plugin.cfg.main.add_headers='sometimes'; + this.plugin.cfg.main.add_headers = 'sometimes' assert.equal( this.plugin.wants_headers_added({ action: 'add header' }), - true - ); + true, + ) assert.equal( this.plugin.wants_headers_added({ action: 'brownlist' }), - false - ); - done(); + false, + ) + done() }) }) @@ -123,128 +131,149 @@ describe('parse_response', function () { it('returns undef on empty string', function (done) { // console.log(this.connection.transaction); - assert.equal( - this.plugin.parse_response('', this.connection), - undefined - ); - done(); + assert.equal(this.plugin.parse_response('', this.connection), undefined) + done() }) it('returns undef on empty object', function (done) { - assert.equal( - this.plugin.parse_response('{}', this.connection), - undefined - ); - done(); + assert.equal(this.plugin.parse_response('{}', this.connection), undefined) + done() }) }) - describe('should_check', function () { - beforeEach(function (done) { - this.plugin = new fixtures.plugin('rspamd'); - this.plugin.register(); - this.connection = connection.createConnection(); - this.connection.init_transaction(); + this.plugin = new fixtures.plugin('rspamd') + this.plugin.register() + this.connection = connection.createConnection() + this.connection.init_transaction() // init defaults - this.plugin.cfg.check.local_ip = false; - this.plugin.cfg.check.private_ip = false; - this.plugin.cfg.check.authenticated = false; + this.plugin.cfg.check.local_ip = false + this.plugin.cfg.check.private_ip = false + this.plugin.cfg.check.authenticated = false - this.connection.remote.is_local = false; - this.connection.remote.is_private = false; - this.connection.notes.auth_user = undefined; + this.connection.remote.is_local = false + this.connection.remote.is_private = false + this.connection.notes.auth_user = undefined done() }) it('checks authenticated', function (done) { - this.connection.notes.auth_user = "username"; - this.plugin.cfg.check.authenticated = true; + this.connection.notes.auth_user = 'username' + this.plugin.cfg.check.authenticated = true - assert.equal(this.plugin.should_check(this.connection), true); - done(); + assert.equal(this.plugin.should_check(this.connection), true) + done() }) it('skips authenticated', function (done) { - this.connection.notes.auth_user = "username"; - this.plugin.cfg.check.authenticated = false; + this.connection.notes.auth_user = 'username' + this.plugin.cfg.check.authenticated = false - assert.equal(this.plugin.should_check(this.connection), false); - done(); + assert.equal(this.plugin.should_check(this.connection), false) + done() }) it('skips relaying', function (done) { - this.connection.relaying = true; - this.plugin.cfg.check.relay = false; + this.connection.relaying = true + this.plugin.cfg.check.relay = false - assert.equal(this.plugin.should_check(this.connection), false); - done(); + assert.equal(this.plugin.should_check(this.connection), false) + done() }) it('checks not relaying', function (done) { - this.connection.relaying = false; - this.plugin.cfg.check.relay = false; + this.connection.relaying = false + this.plugin.cfg.check.relay = false - assert.equal(this.plugin.should_check(this.connection), true); - done(); + assert.equal(this.plugin.should_check(this.connection), true) + done() }) it('checks relaying when enabled', function (done) { - this.connection.relaying = true; - this.plugin.cfg.check.relay = true; + this.connection.relaying = true + this.plugin.cfg.check.relay = true - assert.equal(this.plugin.should_check(this.connection), true); - done(); + assert.equal(this.plugin.should_check(this.connection), true) + done() }) it('checks local IP', function (done) { - this.connection.remote.is_local = true; - this.plugin.cfg.check.local_ip = true; + this.connection.remote.is_local = true + this.plugin.cfg.check.local_ip = true - assert.equal(this.plugin.should_check(this.connection), true); - done(); + assert.equal(this.plugin.should_check(this.connection), true) + done() }) it('skips local IP', function (done) { - this.connection.remote.is_local = true; - this.plugin.cfg.check.local_ip = false; + this.connection.remote.is_local = true + this.plugin.cfg.check.local_ip = false - assert.equal(this.plugin.should_check(this.connection), false); - done(); + assert.equal(this.plugin.should_check(this.connection), false) + done() }) it('checks private IP', function (done) { - this.connection.remote.is_private = true; - this.plugin.cfg.check.private_ip = true; + this.connection.remote.is_private = true + this.plugin.cfg.check.private_ip = true - assert.equal(this.plugin.should_check(this.connection), true); - done(); + assert.equal(this.plugin.should_check(this.connection), true) + done() }) it('skips private IP', function (done) { - this.connection.remote.is_private = true; - this.plugin.cfg.check.private_ip = false; + this.connection.remote.is_private = true + this.plugin.cfg.check.private_ip = false - assert.equal(this.plugin.should_check(this.connection), false); - done(); + assert.equal(this.plugin.should_check(this.connection), false) + done() }) it('checks public ip', function (done) { - assert.equal(this.plugin.should_check(this.connection), true); - done(); + assert.equal(this.plugin.should_check(this.connection), true) + done() }) it('skip localhost if check.local_ip = false and check.private_ip = true', function (done) { - this.connection.remote.is_local = true; - this.connection.remote.is_private = true; + this.connection.remote.is_local = true + this.connection.remote.is_private = true - this.plugin.cfg.check.local_ip = false; - this.plugin.cfg.check.private_ip = true; + this.plugin.cfg.check.local_ip = false + this.plugin.cfg.check.private_ip = true - assert.equal(this.plugin.should_check(this.connection), false); - done(); + assert.equal(this.plugin.should_check(this.connection), false) + done() }) it('checks localhost if check.local_ip = true and check.private_ip = false', function (done) { - this.connection.remote.is_local = true; - this.connection.remote.is_private = true; + this.connection.remote.is_local = true + this.connection.remote.is_private = true - this.plugin.cfg.check.local_ip = true; - this.plugin.cfg.check.private_ip = false; + this.plugin.cfg.check.local_ip = true + this.plugin.cfg.check.private_ip = false - assert.equal(this.plugin.should_check(this.connection), true); - done(); + assert.equal(this.plugin.should_check(this.connection), true) + done() }) }) + +describe.skip('data_post', function () { + beforeEach(_set_up) + + it('streams a message to rspamd and gets response', function (done) { + this.plugin.cfg.main.host = 'mail.example.com' + this.plugin.cfg.main.timeout = 29000 + this.plugin.cfg.check.local_ip = true + this.plugin.cfg.check.private_ip = true + this.plugin.cfg.check.relay = true + + this.connection.remote.ip = '209.85.208.48' + this.connection.hello.host = 'mail-ed1-f48.google.com' + + const specimen = fs.readFileSync('./test/fixtures/spam.eml', 'utf8'); + + for (const line of specimen.split(/\r?\n/g)) { + this.connection.transaction.add_data(`${line}\r\n`); + } + + this.connection.transaction.end_data(); + this.connection.transaction.ensure_body(); + + this.plugin.hook_data_post(() => { + done() + }, + this.connection) + }) +}) \ No newline at end of file