diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 6ccef8e..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "env": { - "node": true, - "es6": true, - "mocha": true - }, - "plugins": [ - "haraka" - ], - "extends": [ "eslint:recommended", "plugin:haraka/recommended" ], - "root": true, - "rules": { - "indent": [2, 4, {"SwitchCase": 1}], - "console": 0, - "no-console": 0 - }, - "globals": { - "OK": true, - "CONT": true, - "DENY": true, - "DENYSOFT": true, - "DENYDISCONNECT": true, - "DENYSOFTDISCONNECT": true - } -} diff --git a/.eslintrc.yaml b/.eslintrc.yaml new file mode 100644 index 0000000..b76f332 --- /dev/null +++ b/.eslintrc.yaml @@ -0,0 +1,24 @@ +env: + node: true + es6: true + mocha: true + +plugins: + - haraka + +extends: + - eslint:recommended + - plugin:haraka/recommended + +root: true + +rules: + indent: [ error, 2, { SwitchCase: 1 } ] + +globals: + OK: true + CONT: true + DENY: true + DENYSOFT: true + DENYDISCONNECT: true + DENYSOFTDISCONNECT: true diff --git a/.gitignore b/.gitignore index 5148e52..eba3cc2 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ jspm_packages # Optional REPL history .node_repl_history + +package-lock.json \ No newline at end of file diff --git a/Changes.md b/Changes.md index c794475..6cdf95b 100644 --- a/Changes.md +++ b/Changes.md @@ -1,23 +1,34 @@ + +## 1.1.N - 2020-MM-DD + +- replace nodeunit with mocha + + ## 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) diff --git a/appveyor.yml b/appveyor.yml index edbfbe0..4910e60 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,5 @@ environment: - nodejs_version: "8" + nodejs_version: "10" install: - ps: Install-Product node $env:nodejs_version diff --git a/index.js b/index.js index 56dc2c6..dc7259a 100644 --- a/index.js +++ b/index.js @@ -7,452 +7,451 @@ const http = require('http'); 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', - ], - }, () => { - plugin.load_rspamd_ini(); - }); - - if (!plugin.cfg.reject.message) { - plugin.cfg.reject.message = 'Detected as spam'; - } - - if (!plugin.cfg.soft_reject.message) { - plugin.cfg.soft_reject.message = 'Deferred by policy'; + 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', + ], + }, () => { + plugin.load_rspamd_ini(); + }); + + if (!plugin.cfg.reject.message) { + plugin.cfg.reject.message = 'Detected as spam'; + } + + if (!plugin.cfg.soft_reject.message) { + plugin.cfg.soft_reject.message = 'Deferred by policy'; + } + + if (!plugin.cfg.spambar) { + plugin.cfg.spambar = { positive: '+', negative: '-', neutral: '/' }; + } + + if (!plugin.cfg.main.port) plugin.cfg.main.port = 11333; + if (!plugin.cfg.main.host) plugin.cfg.main.host = 'localhost'; + + if (!plugin.cfg.main.add_headers) { + if (plugin.cfg.main.always_add_headers === true) { + plugin.cfg.main.add_headers = 'always'; } - - if (!plugin.cfg.spambar) { - plugin.cfg.spambar = { positive: '+', negative: '-', neutral: '/' }; - } - - if (!plugin.cfg.main.port) plugin.cfg.main.port = 11333; - if (!plugin.cfg.main.host) plugin.cfg.main.host = 'localhost'; - - if (!plugin.cfg.main.add_headers) { - if (plugin.cfg.main.always_add_headers === true) { - plugin.cfg.main.add_headers = 'always'; - } - else { - plugin.cfg.main.add_headers = 'sometimes'; - } + else { + plugin.cfg.main.add_headers = 'sometimes'; } + } - if (!plugin.cfg.subject) { - plugin.cfg.subject = "[SPAM] %s"; - } + if (!plugin.cfg.subject) { + plugin.cfg.subject = "[SPAM] %s"; + } } exports.get_options = function (connection) { - const plugin = this; - - // 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 (plugin.cfg.main.unix_socket) { - options.socketPath = plugin.cfg.main.unix_socket; - } - else { - options.port = plugin.cfg.main.port; - options.host = plugin.cfg.main.host; + const plugin = this; + + // 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 (plugin.cfg.main.unix_socket) { + options.socketPath = plugin.cfg.main.unix_socket; + } + else { + options.port = plugin.cfg.main.port; + options.host = plugin.cfg.main.host; + } + + if (connection.notes.auth_user) { + options.headers.User = connection.notes.auth_user; + } + + if (connection.remote.ip) options.headers.IP = connection.remote.ip; + + const fcrdns = connection.results.get('fcrdns'); + if (fcrdns && fcrdns.fcrdns && fcrdns.fcrdns[0]) { + options.headers.Hostname = fcrdns.fcrdns[0]; + } + else { + if (connection.remote.host) { + options.headers.Hostname = connection.remote.host; } + } - if (connection.notes.auth_user) { - options.headers.User = connection.notes.auth_user; - } - - if (connection.remote.ip) options.headers.IP = connection.remote.ip; + if (connection.hello.host) options.headers.Helo = connection.hello.host; - const fcrdns = connection.results.get('fcrdns'); - if (fcrdns && fcrdns.fcrdns && fcrdns.fcrdns[0]) { - options.headers.Hostname = fcrdns.fcrdns[0]; - } - else { - if (connection.remote.host) { - options.headers.Hostname = connection.remote.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'); if (spf && spf.result) { - options.headers.SPF = { result: spf.result.toLowerCase() }; + options.headers.SPF = { result: spf.result.toLowerCase() }; } - else { - spf = connection.results.get('spf'); - if (spf && spf.result) { - options.headers.SPF = { result: spf.result.toLowerCase() }; - } + } + + if (connection.transaction.mail_from) { + const mfaddr = connection.transaction.mail_from.address().toString(); + if (mfaddr) { + options.headers.From = mfaddr; } + } - if (connection.transaction.mail_from) { - const mfaddr = connection.transaction.mail_from.address().toString(); - if (mfaddr) { - options.headers.From = mfaddr; - } + const rcpts = connection.transaction.rcpt_to; + if (rcpts) { + options.headers.Rcpt = []; + for (let i=0; i < rcpts.length; i++) { + options.headers.Rcpt.push(rcpts[i].address()); } - const rcpts = connection.transaction.rcpt_to; - if (rcpts) { - options.headers.Rcpt = []; - for (let i=0; i < rcpts.length; i++) { - options.headers.Rcpt.push(rcpts[i].address()); - } - - // for per-user options - if (rcpts.length === 1) { - options.headers['Deliver-To'] = options.headers.Rcpt[0]; - } + // for per-user options + if (rcpts.length === 1) { + options.headers['Deliver-To'] = options.headers.Rcpt[0]; } + } - if (connection.transaction.uuid) - options.headers['Queue-Id'] = connection.transaction.uuid; + if (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; - } + if (connection.tls.enabled) { + 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) { - const plugin = this; + const plugin = this; - if (!plugin.cfg.smtp_message.enabled || !r.data.messages) return; - if (typeof(r.data.messages) !== 'object') return; - if (!r.data.messages.smtp_message) return; + if (!plugin.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) { - const plugin = this; + const plugin = this; - if (!plugin.cfg.rewrite_subject.enabled) return false; - if (data.action !== 'rewrite subject') return false; + if (!plugin.cfg.rewrite_subject.enabled) return false; + if (data.action !== 'rewrite subject') return false; - const rspamd_subject = data.subject || plugin.cfg.subject; - const old_subject = connection.transaction.header.get('Subject') || ''; - const new_subject = rspamd_subject.replace('%s', old_subject); + const rspamd_subject = data.subject || plugin.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) { - const plugin = this; + const plugin = this; - if (!plugin.cfg.dkim.enabled) return; - if (!data['dkim-signature']) return; + if (!plugin.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) { - const plugin = this; + const plugin = this; - if (!plugin.cfg.rmilter_headers.enabled) return; - if (!data.milter) return; + if (!plugin.cfg.rmilter_headers.enabled) return; + if (!data.milter) return; - if (data.milter.remove_headers) { - Object.keys(data.milter.remove_headers).forEach((key) => { - connection.transaction.remove_header(key); - }) - } - - if (data.milter.add_headers) { - Object.keys(data.milter.add_headers).forEach((key) => { - const header_value = data.milter.add_headers[key]; - if (!header_value) return; - - if (typeof header_value === 'object') { - connection.transaction.add_header(key, header_value.value); - } - else { - connection.transaction.add_header(key, header_value); - } - }) - } + if (data.milter.remove_headers) { + Object.keys(data.milter.remove_headers).forEach((key) => { + connection.transaction.remove_header(key); + }) + } + + if (data.milter.add_headers) { + Object.keys(data.milter.add_headers).forEach((key) => { + const header_value = data.milter.add_headers[key]; + if (!header_value) return; + + if (typeof header_value === 'object') { + connection.transaction.add_header(key, header_value.value); + } + else { + connection.transaction.add_header(key, header_value); + } + }) + } } 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; - next(code, msg); - } + let calledNext=false; + function nextOnce (code, msg) { + clearTimeout(timer); + if (calledNext) return; + calledNext=true; + next(code, msg); + } - timer = setTimeout(() => { - if (!connection) return; - if (!connection.transaction) return; - connection.transaction.results.add(plugin, {err: 'timeout'}); - nextOnce(); - }, timeout * 1000); + timer = setTimeout(() => { + if (!connection) return; + if (!connection.transaction) return; + connection.transaction.results.add(plugin, {err: 'timeout'}); + nextOnce(); + }, timeout * 1000); - const start = Date.now(); + const start = Date.now(); - const req = http.request(plugin.get_options(connection), (res) => { - let rawData = ''; + const req = http.request(plugin.get_options(connection), (res) => { + let rawData = ''; - res.on('data', (chunk) => { rawData += chunk; }); + res.on('data', (chunk) => { rawData += chunk; }); - res.on('end', () => { - const r = plugin.parse_response(rawData, connection); - if (!r || !r.data || !r.log) return nextOnce(); + res.on('end', () => { + const r = plugin.parse_response(rawData, connection); + if (!r || !r.data || !r.log) 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; - if (!connection.transaction) return nextOnce(); + if (!connection.transaction) return nextOnce(); - 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)); - } - 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); + if (plugin.cfg.soft_reject.enabled && r.data.action === 'soft reject') { + 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(); - } - }); - }) - - req.on('error', (err) => { - if (!connection || !connection.transaction) return; - connection.transaction.results.add(plugin, { err: err.message}); nextOnce(); + } }); + }) - connection.transaction.message_stream.pipe(req); - // pipe calls req.end() asynchronously + req.on('error', (err) => { + if (!connection || !connection.transaction) return; + connection.transaction.results.add(plugin, { err: err.message}); + nextOnce(); + }); + + connection.transaction.message_stream.pipe(req); + // pipe calls req.end() asynchronously } exports.should_check = function (connection) { - const plugin = this; + const plugin = this; - let result = true; // default + let result = true; // default - if (plugin.cfg.check.authenticated == false && connection.notes.auth_user) { - connection.transaction.results.add(plugin, { skip: 'authed'}); - result = false; - } + if (plugin.cfg.check.authenticated == false && connection.notes.auth_user) { + connection.transaction.results.add(plugin, { skip: 'authed'}); + result = false; + } - if (plugin.cfg.check.relay == false && connection.relaying) { - connection.transaction.results.add(plugin, { skip: 'relay'}); - result = false; - } + if (plugin.cfg.check.relay == false && connection.relaying) { + connection.transaction.results.add(plugin, { skip: 'relay'}); + result = false; + } - if (plugin.cfg.check.local_ip == false && connection.remote.is_local) { - connection.transaction.results.add(plugin, { skip: 'local_ip'}); - result = false; - } + if (plugin.cfg.check.local_ip == false && connection.remote.is_local) { + connection.transaction.results.add(plugin, { skip: 'local_ip'}); + result = false; + } - if (plugin.cfg.check.private_ip == false && connection.remote.is_private) { - if (plugin.cfg.check.local_ip == true && connection.remote.is_local) { - // local IPs are included in private IPs - } - else { - connection.transaction.results.add(plugin, { skip: 'private_ip'}); - result = false; - } + if (plugin.cfg.check.private_ip == false && connection.remote.is_private) { + if (plugin.cfg.check.local_ip == true && connection.remote.is_local) { + // local IPs are included in private IPs + } + else { + connection.transaction.results.add(plugin, { skip: 'private_ip'}); + result = false; } + } - return result; + return result; } exports.wants_reject = function (connection, data) { - const plugin = this; + const plugin = this; - if (data.action !== 'reject') return false; + if (data.action !== 'reject') return false; - if (connection.notes.auth_user) { - if (plugin.cfg.reject.authenticated == false) return false; - } - else { - if (plugin.cfg.reject.spam == false) return false; - } + if (connection.notes.auth_user) { + if (plugin.cfg.reject.authenticated == false) return false; + } + else { + if (plugin.cfg.reject.spam == false) return false; + } - return true; + return true; } exports.wants_headers_added = function (rspamd_data) { - const plugin = this; + const plugin = this; - if (plugin.cfg.main.add_headers === 'never') return false; - if (plugin.cfg.main.add_headers === 'always') return true; + if (plugin.cfg.main.add_headers === 'never') return false; + if (plugin.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; + // implicit add_headers=sometimes, based on rspamd response + if (rspamd_data.action === 'add header') return true; + return false; } exports.get_clean = function (data, connection) { - const plugin = this; - const clean = { symbols: {} }; - - if (data.symbols) { - 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; - } - // unhandled type - connection.logerror(plugin, a); - }) + const plugin = this; + const clean = { symbols: {} }; + + if (data.symbols) { + 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; + } + // unhandled type + connection.logerror(plugin, a); + }) + } + + // objects that may exist + ['action', 'is_skipped', 'required_score', 'score'].forEach((key) => { + switch (typeof data[key]) { + case 'boolean': + case 'number': + case 'string': + clean[key] = data[key]; + break; + default: + connection.loginfo(plugin, "skipping unhandled: " + typeof data[key]); } + }); - // objects that may exist - ['action', 'is_skipped', 'required_score', 'score'].forEach((key) => { - switch (typeof data[key]) { - case 'boolean': - case 'number': - case 'string': - clean[key] = data[key]; - break; - default: - connection.loginfo(plugin, "skipping unhandled: " + typeof data[key]); - } - }); + // arrays which might be present + ['urls', 'emails', 'messages'].forEach(b => { + // collapse to comma separated string, so values get logged + if (!data[b]) return; - // arrays which might be present - ['urls', 'emails', 'messages'].forEach(b => { - // collapse to comma separated string, so values get logged - if (!data[b]) return; - - if (data[b].length) { - clean[b] = data[b].join(','); - return; - } - - if (typeof(data[b]) == 'object') { - // 'messages' is probably a dictionary - Object.keys(data[b]).map((k) => { - return `${k} : ${data[b][k]}`; - }).join(','); - } - }); + if (data[b].length) { + clean[b] = data[b].join(','); + return; + } + + if (typeof(data[b]) == 'object') { + // 'messages' is probably a dictionary + Object.keys(data[b]).map((k) => { + return `${k} : ${data[b][k]}`; + }).join(','); + } + }); - return clean; + return clean; } exports.parse_response = function (rawData, connection) { - const plugin = this; + const plugin = this; - if (!rawData) return; + if (!rawData) return; - let data; - try { - data = JSON.parse(rawData); - } - catch (err) { - connection.transaction.results.add(plugin, { - err: 'parse failure: ' + err.message - }); - return; - } + let data; + try { + data = JSON.parse(rawData); + } + catch (err) { + connection.transaction.results.add(plugin, { + 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(plugin, { - err: data.error - }); - return; - } + if (Object.keys(data).length === 1 && data.error) { + connection.transaction.results.add(plugin, { + err: data.error + }); + return; + } - return { - 'data' : data, - 'log' : plugin.get_clean(data, connection), - }; + return { + 'data' : data, + 'log' : plugin.get_clean(data, connection), + }; } exports.add_headers = function (connection, data) { - const plugin = this; - const cfg = plugin.cfg; - - if (!plugin.wants_headers_added(data)) return; - - if (cfg.header && cfg.header.bar) { - 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 || '-'; - } - for (let i = 0; i < spamBarScore; i++) { - spamBar += spamBarChar; - } - connection.transaction.remove_header(cfg.header.bar); - connection.transaction.add_header(cfg.header.bar, spamBar); + const plugin = this; + const cfg = plugin.cfg; + + if (!plugin.wants_headers_added(data)) return; + + if (cfg.header && cfg.header.bar) { + let spamBar = ''; + let spamBarScore = 1; + let spamBarChar = cfg.spambar.neutral || '/'; + if (data.score >= 1) { + spamBarScore = Math.floor(data.score); + spamBarChar = cfg.spambar.positive || '+'; } - - if (cfg.header && cfg.header.report) { - const prettySymbols = []; - for (const k in data.symbols) { - if (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(' ')); + else if (data.score <= -1) { + spamBarScore = Math.floor(data.score * -1); + spamBarChar = cfg.spambar.negative || '-'; } - - if (cfg.header && cfg.header.score) { - connection.transaction.remove_header(cfg.header.score); - connection.transaction.add_header(cfg.header.score, '' + data.score); + for (let i = 0; i < spamBarScore; i++) { + spamBar += spamBarChar; + } + connection.transaction.remove_header(cfg.header.bar); + connection.transaction.add_header(cfg.header.bar, spamBar); + } + + if (cfg.header && cfg.header.report) { + const prettySymbols = []; + for (const k in data.symbols) { + if (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(' ')); + } + + if (cfg.header && cfg.header.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 0ea053b..6cd2cb5 100644 --- a/package.json +++ b/package.json @@ -4,10 +4,10 @@ "description": "Haraka plugin for rspamd", "main": "index.js", "scripts": { - "lint": "./node_modules/.bin/eslint *.js test/**/*.js", + "lint": "npx eslint *.js test/**/*.js", "lintfix": "./node_modules/.bin/eslint --fix *.js test/**/*.js", - "cover": "./node_modules/.bin/istanbul cover ./node_modules/.bin/nodeunit", - "test": "./node_modules/.bin/nodeunit" + "cover": "npx istanbul cover npm run test", + "test": "npx mocha" }, "repository": { "type": "git", @@ -25,10 +25,10 @@ }, "homepage": "https://github.com/haraka/haraka-plugin-rspamd#readme", "devDependencies": { - "eslint": ">=3", + "eslint": ">=4", "eslint-plugin-haraka": "*", "haraka-test-fixtures": "*", - "nodeunit": "*" + "mocha": "*" }, "dependencies": { "haraka-dsn": "*" diff --git a/run_tests b/run_tests deleted file mode 100644 index 4931840..0000000 --- a/run_tests +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env node -'use strict' -try { - var reporter = require('nodeunit').reporters.default; -} -catch (e) { - console.log(` -Error: ${e.message} - -Cannot find nodeunit. Did you run 'npm install'? -`) - process.exit() -} - -process.chdir(__dirname); - -reporter.run([ 'test' ], undefined, (err) => { - process.exit(((err) ? 1 : 0)); -}); diff --git a/test/.eslintrc.json b/test/.eslintrc.json deleted file mode 100644 index 8d978a0..0000000 --- a/test/.eslintrc.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "env": { - "mocha": true, - "node": true, - "es6": true - }, - "globals": { - "OK": true, - "CONT": true, - "DENY": true, - "DENYSOFT": true, - "DENYDISCONNECT": true, - "DENYSOFTDISCONNECT": true - } -} diff --git a/test/index.js b/test/index.js index a166acb..ca266eb 100644 --- a/test/index.js +++ b/test/index.js @@ -1,152 +1,148 @@ 'use strict'; -// var Address = require('address-rfc2821'); -const fixtures = require('haraka-test-fixtures'); +const assert = require('assert') +const fixtures = require('haraka-test-fixtures'); const connection = fixtures.connection; -const _set_up = function (done) { +function _set_up (done) { + this.plugin = new fixtures.plugin('rspamd'); + this.plugin.register(); + this.connection = connection.createConnection(); + this.connection.init_transaction(); - this.plugin = new fixtures.plugin('rspamd'); + done(); +} + +describe('register', function () { + + beforeEach(_set_up) + + it('loads the rspamd plugin', function (done) { + assert.equal('rspamd', this.plugin.name); + done(); + }) + + it('register loads rspamd.ini', function (done) { this.plugin.register(); - this.connection = connection.createConnection(); - this.connection.init_transaction(); + 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); + // assert.ok(!this.plugin.score_too_high(this.connection, {score: 5})); done(); -} + }) + + it('adds a header to a message with positive score', function (done) { + const test_data = { + score: 1.1, + symbols: { + FOO: { + name: 'FOO', + score: 0.100000, + description: 'foo', + options: ['foo', 'bar'], + }, + BAR: { + name: 'BAR', + score: 1.0, + description: 'bar', + } + } + }; + this.plugin.cfg.main.add_headers = 'always'; + this.plugin.add_headers(this.connection, test_data); + assert.equal(this.connection.transaction.header.headers['X-Rspamd-Score'], '1.1'); + assert.equal(this.connection.transaction.header.headers['X-Rspamd-Bar'], '+'); + assert.equal(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); + // console.log(this.connection.transaction.header); + assert.equal(this.connection.transaction.header.headers['X-Rspamd-Score'], '-1'); + assert.equal(this.connection.transaction.header.headers['X-Rspamd-Bar'], '-'); + done(); + }) +}) -exports.register = { - setUp : _set_up, - 'loads the rspamd plugin': function (test) { - test.expect(1); - test.equal('rspamd', this.plugin.name); - test.done(); - }, - 'register loads rspamd.ini': function (test) { - test.expect(2); - this.plugin.register(); - test.ok(this.plugin.cfg); - test.equal(true, this.plugin.cfg.reject.spam); - test.done(); - }, -} -exports.load_rspamd_ini = { - setUp : _set_up, - 'loads rspamd.ini': function (test) { - test.expect(1); - this.plugin.load_rspamd_ini(); - test.ok(this.plugin.cfg.header.bar); - test.done(); - }, -} +describe('wants_headers_added', function () { -exports.add_headers = { - setUp : _set_up, - 'add_headers exists as function': function (test) { - test.expect(1); - // console.log(this.plugin.cfg); - test.equal('function', typeof this.plugin.add_headers); - // test.ok(!this.plugin.score_too_high(this.connection, {score: 5})); - test.done(); - }, - 'adds a header to a message with positive score': function (test) { - test.expect(3); - const test_data = { - score: 1.1, - symbols: { - FOO: { - name: 'FOO', - score: 0.100000, - description: 'foo', - options: ['foo', 'bar'], - }, - BAR: { - name: 'BAR', - score: 1.0, - description: 'bar', - } - } - }; - this.plugin.cfg.main.add_headers = 'always'; - this.plugin.add_headers(this.connection, test_data); - test.equal(this.connection.transaction.header.headers['X-Rspamd-Score'], '1.1'); - test.equal(this.connection.transaction.header.headers['X-Rspamd-Bar'], '+'); - test.equal(this.connection.transaction.header.headers['X-Rspamd-Report'], 'FOO(0.1) BAR(1)'); - test.done(); - }, - 'adds a header to a message with negative score': function (test) { - test.expect(2); - const 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); - test.equal(this.connection.transaction.header.headers['X-Rspamd-Score'], '-1'); - test.equal(this.connection.transaction.header.headers['X-Rspamd-Bar'], '-'); - test.done(); - } -} + beforeEach(_set_up) -exports.wants_headers_added = { - setUp : _set_up, - 'wants no headers when add_headers=never': function (test) { - test.expect(1); - this.plugin.cfg.main.add_headers='never'; - test.equal( - this.plugin.wants_headers_added({ action: 'add header' }), - false - ); - test.done(); - }, - 'always wants no headers when add_headers=always': function (test) { - test.expect(1); - this.plugin.cfg.main.add_headers='always'; - test.equal( - this.plugin.wants_headers_added({ action: 'beat it' }), - true - ); - test.done(); - }, - 'wants headers when rspamd response indicates, add_headers=sometimes': function (test) { - test.expect(2); - this.plugin.cfg.main.add_headers='sometimes'; - test.equal( - this.plugin.wants_headers_added({ action: 'add header' }), - true - ); - test.equal( - this.plugin.wants_headers_added({ action: 'brownlist' }), - false - ); - test.done(); - } -} + it('wants no headers when add_headers=never', function (done) { + this.plugin.cfg.main.add_headers='never'; + assert.equal( + this.plugin.wants_headers_added({ action: 'add header' }), + 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(); + }) + + it('wants headers when rspamd response indicates, add_headers=sometimes', function (done) { + this.plugin.cfg.main.add_headers='sometimes'; + assert.equal( + this.plugin.wants_headers_added({ action: 'add header' }), + true + ); + assert.equal( + this.plugin.wants_headers_added({ action: 'brownlist' }), + false + ); + done(); + }) +}) + +describe('parse_response', function () { + beforeEach(_set_up) + + it('returns undef on empty string', function (done) { + // console.log(this.connection.transaction); + assert.equal( + this.plugin.parse_response('', this.connection), + undefined + ); + done(); + }) -exports.parse_response = { - setUp : _set_up, - 'returns undef on empty string': function (test) { - test.expect(1); - // console.log(this.connection.transaction); - test.equal( - this.plugin.parse_response('', this.connection), - undefined - ); - test.done(); - }, - 'returns undef on empty object': function (test) { - test.expect(1); - test.equal( - this.plugin.parse_response('{}', this.connection), - undefined - ); - test.done(); - }, -} + it('returns undef on empty object', function (done) { + assert.equal( + this.plugin.parse_response('{}', this.connection), + undefined + ); + done(); + }) +}) -function _check_setup (done) { +describe('should_check', function () { + + beforeEach(function (done) { this.plugin = new fixtures.plugin('rspamd'); this.plugin.register(); this.connection = connection.createConnection(); @@ -162,107 +158,93 @@ function _check_setup (done) { this.connection.notes.auth_user = undefined; done() -} + }) + + it('checks authenticated', function (done) { + this.connection.notes.auth_user = "username"; + this.plugin.cfg.check.authenticated = true; -exports.should_check = { - setUp : _check_setup, - 'checks authenticated': function (test) { - this.connection.notes.auth_user = "username"; - this.plugin.cfg.check.authenticated = true; - - test.expect(1); - test.equal(this.plugin.should_check(this.connection), true); - test.done(); - }, - 'skips authenticated': function (test) { - this.connection.notes.auth_user = "username"; - this.plugin.cfg.check.authenticated = false; - - test.expect(1); - test.equal(this.plugin.should_check(this.connection), false); - test.done(); - }, - 'skips relaying': function (test) { - this.connection.relaying = true; - this.plugin.cfg.check.relay = false; - - test.expect(1); - test.equal(this.plugin.should_check(this.connection), false); - test.done(); - }, - 'checks not relaying': function (test) { - this.connection.relaying = false; - this.plugin.cfg.check.relay = false; - - test.expect(1); - test.equal(this.plugin.should_check(this.connection), true); - test.done(); - }, - 'checks relaying when enabled': function (test) { - this.connection.relaying = true; - this.plugin.cfg.check.relay = true; - - test.expect(1); - test.equal(this.plugin.should_check(this.connection), true); - test.done(); - }, - 'checks local IP': function (test) { - this.connection.remote.is_local = true; - this.plugin.cfg.check.local_ip = true; - - test.expect(1); - test.equal(this.plugin.should_check(this.connection), true); - test.done(); - }, - 'skips local IP': function (test) { - this.connection.remote.is_local = true; - this.plugin.cfg.check.local_ip = false; - - test.expect(1); - test.equal(this.plugin.should_check(this.connection), false); - test.done(); - }, - 'checks private IP': function (test) { - this.connection.remote.is_private = true; - this.plugin.cfg.check.private_ip = true; - - test.expect(1); - test.equal(this.plugin.should_check(this.connection), true); - test.done(); - }, - 'skips private IP': function (test) { - this.connection.remote.is_private = true; - this.plugin.cfg.check.private_ip = false; - - test.expect(1); - test.equal(this.plugin.should_check(this.connection), false); - test.done(); - }, - 'checks public ip': function (test) { - test.expect(1); - test.equal(this.plugin.should_check(this.connection), true); - test.done(); - }, - 'skip localhost if check.local_ip = false and check.private_ip = true': function (test) { - 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; - - test.expect(1); - test.equal(this.plugin.should_check(this.connection), false); - test.done(); - }, - 'checks localhost if check.local_ip = true and check.private_ip = false': function (test) { - 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; - - test.expect(1); - test.equal(this.plugin.should_check(this.connection), true); - test.done(); - }, -} \ No newline at end of file + 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; + + 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; + + 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; + + 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; + + 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; + + 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; + + 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; + + 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; + + 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(); + }) + 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.plugin.cfg.check.local_ip = false; + this.plugin.cfg.check.private_ip = true; + + 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.plugin.cfg.check.local_ip = true; + this.plugin.cfg.check.private_ip = false; + + assert.equal(this.plugin.should_check(this.connection), true); + done(); + }) +})