diff --git a/Changes.md b/Changes.md index fac831dc1..6f54b44fe 100644 --- a/Changes.md +++ b/Changes.md @@ -8,28 +8,27 @@ #### Changed +- .gitignore: add config/me and config/*.pem +- auth_base: enable disabling constrain_sender at runtime #3298 +- auth_base: skip constrain_sender when auth user has no domain #3319 - avg: repackaged as NPM module #3347 - bounce: repackaged plugin as NPM module #3341 - connection: check remote is connected before queue #3338 -- transaction: init with conn.init_transaction, always pass in cfg #3315 -- check for local_mx only when default route is used #3307 + - support IPv6 when setting remote.is_private #3295 + - in setTLS, replace forEach with for...of + - NOTE: remove a handful of 3.0 sunset property names #3315 +- deps: bump all versions to latest #3303, #3344 - dkim: repackaged as NPM module #3311 - new NPM plugin dns-list, repackages dnsbl, dnswl, backscatterer #3313 -- test: add a connection.response test case with DSN #3305 -- deps: bump all versions to latest #3303, #3344 - when using message-stream, don't send default options #3290 -- auth_base: enable disabling constrain_sender at runtime #3298 -- auth_base: skip constrain_sender when auth user has no domain #3319 - rcpt_to.host_list: add connection ID to log messages #3322 -- connection: support IPv6 when setting remote.is_private #3295 - - in setTLS, replace forEach with for...of - - NOTE: remove a handful of 3.0 sunset property names #3315 - line_socket: remove unused callback #3344 - logger: don't load outbound (race condition). Instead, set name property #3322 - logger: extend add_log_methods to Classes (connection, plugins, hmail) #3322 - logger: when logging via `logger` methods, use short names #3322 - logger: check Object.hasOwn to avoid circular deps - outbound + - check for local_mx only when default route is used #3307 - client_pool: use tls_socket directly (shed line_socket) - client_pool: sock.name is now JSON of socket args - client_pool.get_client & release_client: arity of 5 -> 2 @@ -52,6 +51,7 @@ - mail_from.resolvable: refactored, leaning on improved net_utils #3322 - fixes haraka/haraka-net-utils#88 - smtp_client: pass connect_timeout, maybe fixes #3281 +- spamassassin: repackaged as NPM module #3348 - style(es6): more for...of loops - deps: moved attachment, spf, & dkim into optional deps - doc(Plugins.md): update registry @@ -64,7 +64,8 @@ - shed dependency on caolin/async & openssl-wrapper - get_certs_dir is now async - completely refactored. -- .gitignore: add config/me and config/*.pem +- transaction: init with conn.init_transaction, always pass in cfg #3315 +- test: add a connection.response test case with DSN #3305 - test: convert test runner to mocha - test: rename tests -> test (where test runner expect) #3340 diff --git a/config/spamassassin.ini b/config/spamassassin.ini deleted file mode 100644 index 99cb0ac6a..000000000 --- a/config/spamassassin.ini +++ /dev/null @@ -1,56 +0,0 @@ -; How does Haraka connect to the SpamAssassin spamd daemon? -; TCP/IP: 127.0.0.1:783 -; socket: /var/run/spamd/spamd.sock -spamd_socket=127.0.0.1:783 - -; the username we tell spamd the message is to (default: default) -;spamd_user=first-recipient (see docs) -;spamd_user= - -; messages larger than this are not scored by SA -max_size=500000 - -; Munge the subject of messages with a score higher than.. -; munge_subject_threshold=5 -subject_prefix=*** SPAM *** - -; what to do with incoming messages with X-Spam-* headers -; options are: rename, drop, keep -old_headers_action=rename - -; use the SpamAssassin 3.0+ syntax in X-Spam-Status header -; modern: No, score=0.8 required=8.0 tests=... -; legacy: No, hits=0.8 required=8.0 tests=... -modern_status_syntax=1 - -; Reject all messages with more than this many hits -; reject_threshold=10 - -; when a connection has relay privileges, the rejection limit -; relay_reject_threshold=7 - -; How long should we wait for SpamAssassin to answer the socket -; in seconds (default: 30) -;connect_timeout= - -; How long should we wait for a result from SpamAssassin -; in seconds (default: 300) -;results_timeout= - -; Merge SpamAssassin's headers into the message -;add_headers=true - -; the header that is sent to spamc -;spamc_auth_header = X-Haraka-Relay - -[check] -;authenticated=true -;private_ip=true -;local_ip=true -;relay=true - -[defer] -; Set to true to return DENYSOFT on errors, connection timeouts, or scanning timeouts -;error=false -;connect_timeout=false -;scan_timeout=false diff --git a/docs/plugins/spamassassin.md b/docs/plugins/spamassassin.md deleted file mode 100644 index 5fcd316b9..000000000 --- a/docs/plugins/spamassassin.md +++ /dev/null @@ -1,180 +0,0 @@ -spamassassin -============ - -This plugin implements the spamd protocol and will send messages to -spamd for scoring. - -Configuration -------------- - -spamassassin.ini - -- spamd\_socket = \[host:port | /path/to/socket\] *optional* - - Default: localhost:783 - - Host or path to socket where spamd is running. - -- spamd\_user = \[user\] *optional* - - Default: default - - Username to pass to spamd. This is useful when you are running - spamd with virtual users. - - You can also pass this value in dynamically by setting: - - 1. `connection.transaction.notes.spamd_user` in another plugin. - - 2. The special username: _first-recipient_. The first envelope recipient - will be used as the username. - - 3. the special username _all-recipients_ may eventually be supported. See - the get_spamd_username function in the plugin. - -- max\_size = N *optional* - - Default: 500000 - - Maximum size of messages (in bytes) to send to spamd. - Messages over this size will be skipped. - -- reject\_threshold = N *optional* - - Default: none (do not reject any mail) - - SpamAssassin score at which the mail should be rejected. - -- relay\_reject\_threshold = N *optional* - - Default: none - - As above, except this threshold only applies to connections - that are relays (e.g. AUTH) where connection.relaying = true. - This is used to set a *lower* thresold at which to reject mail - from these hosts to prevent sending outbound spam. - - If this is not set, then the `reject_thresold` value is used. - -- munge\_subject\_threshold = N *optional* - - Default: none (do not munge the subject) - - Score at which the subject should be munged (prefixed). - -- subject\_prefix = \[prefix\] *optional* - - Default: *** SPAM *** - - Prefix to use when munging the subject. - -- old\_headers\_action = \[rename | drop | keep\] *optional* - - Default: rename - - If old X-Spam-\* headers are in the email, what do we do with them? - - `rename` them to X-Old-Spam-\*. - - `drop` will delete them. - - `keep` will keep them (new X-Spam-\* headers appear lower down in - the headers then). - -- connect\_timeout = N *optional* - - Default: 30 - - Time in seconds to wait for a connection to spamd - -- results\_timeout = N *optional* - - Default: 300 - - Time in seconds to wait for results from spamd - - -[check] -======= - -The optional check section can allow skipping SpamAssassin check for remote connection -meeting following criteria. - -- authenticated - - Default: true - - If true, messages from authenticated users will be scored. - -- private\_ip - - Default: true - - If true, messages from private IPs will be scored. - -- local\_ip - - Default: true - - If true, messages from localhost will be scored. - -- relay - - Default: true - - If true, messages that are to be relayed will be scored. - -[defer] -======= - -The optional defer section can allow returning a DENYSOFT status back to the -client. Setting these to true will force the client to retry later in cases where -spamassassin is not responding properly. If set to false, then the errors -will be ignored and message processing will continue. - -- error - - Default: false - - If true, return DENYSOFT on socket errors - -- connect\_timeout - - Default: false - - If true, return DENYSOFT on socket connection timeouts - -- scan\_timeout - - Default: false - - If true, return DENYSOFT on scan timeouts - -Extras -====== - -A SpamAssassin plugin can be found in the `contrib` directory. -The `Haraka.\[pm|cf\]` files should be placed in the SpamAssassin local -site rules directory (/etc/mail/spamassassin on Linux), spamd should be -restarted and the plugin will make spamd output the Haraka UUID as part -of its log output to aid debugging when searching the mail logs. - - -Changes --------------- - -This plugin now passes the X-Spam-\* headers generated by SA through -unaltered. You can control the presence and appearance of X-Spam-\* -headers by editing your SpamAssassin config. - -The default headers added by SpamAssassin are: - - add_header all Checker-Version SpamAssassin _VERSION_ (_SUBVERSION_) on _HOSTNAME_ - add_header spam Flag _YESNOCAPS_ - add_header all Level _STARS(\*)_ - add_header all Status "_YESNO_, score=_SCORE_ required=_REQD_ tests=_TESTS_ autolearn=_AUTOLEARN_ version=_VERSION_" - -Other headers options you might find interesting or useful are: - - add_header all DCC _DCCB_: _DCCR_ - add_header all Tests _TESTS_ diff --git a/package.json b/package.json index 3834ac5b2..52452b5f5 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "haraka-plugin-rcpt-ldap": "^1.1.0", "haraka-plugin-recipient-routes": "^1.2.0", "haraka-plugin-rspamd": "^1.3.1", + "haraka-plugin-spamassassin": "^1.0.0", "haraka-plugin-spf": "1.2.5", "haraka-plugin-syslog": "^1.0.6", "haraka-plugin-uribl": "^1.0.8", diff --git a/plugins/spamassassin.js b/plugins/spamassassin.js deleted file mode 100644 index bc37bd4a6..000000000 --- a/plugins/spamassassin.js +++ /dev/null @@ -1,382 +0,0 @@ -'use strict'; -// Call spamassassin via spamd - -const net = require('node:net') - -const utils = require('haraka-utils'); -const net_utils = require('haraka-net-utils') - -exports.register = function () { - this.load_spamassassin_ini(); -} - -exports.load_spamassassin_ini = function () { - this.cfg = this.config.get('spamassassin.ini', { - booleans: [ - '+add_headers', - '+check.authenticated', - '+check.private_ip', - '+check.local_ip', - '+check.relay', - - '-defer.error', - '-defer.connect_timeout', - '-defer.scan_timeout', - ], - }, () => { - this.load_spamassassin_ini(); - }); - - const defaults = { - spamd_socket: 'localhost:783', - max_size: 500000, - old_headers_action: "rename", - subject_prefix: "*** SPAM ***", - spamc_auth_header: 'X-Haraka-Relay', - }; - - for (const key in defaults) { - if (this.cfg.main[key]) continue; - this.cfg.main[key] = defaults[key]; - } - - for (const item of ['reject_threshold', 'relay_reject_threshold', 'munge_subject_threshold', 'max_size']) { - if (!this.cfg.main[item]) continue; - this.cfg.main[item] = Number(this.cfg.main[item]); - } -} - -exports.hook_data_post = function (next, connection) { - - if (this.should_skip(connection)) return next(); - - const txn = connection.transaction; - txn.remove_header(this.cfg.main.spamc_auth_header); // just to be safe - - const username = this.get_spamd_username(connection); - const headers = this.get_spamd_headers(connection, username); - const socket = this.get_spamd_socket(next, connection, headers); - - const spamd_response = { headers: {} }; - let state = 'line0'; - let last_header; - const start = Date.now(); - - socket.on('line', line => { - connection.logprotocol(this, `Spamd C: ${line} state=${state}`); - line = line.replace(/\r?\n/, ''); - if (state === 'line0') { - spamd_response.line0 = line; - state = 'response'; - } - else if (state === 'response') { - if (line.match(/\S/)) { - const matches = line.match(/Spam: (True|False) ; (-?\d+\.\d) \/ (-?\d+\.\d)/); - if (matches) { - spamd_response.flag = matches[1]; - spamd_response.score = matches[2]; - spamd_response.hits = matches[2]; // backwards compat - spamd_response.reqd = matches[3]; - spamd_response.flag = spamd_response.flag === 'True' ? 'Yes' : 'No'; - } - } - else { - state = 'headers'; - } - } - else if (state === 'headers') { - const m = line.match(/^X-Spam-([\x21-\x39\x3B-\x7E]+):\s*(.*)/); - if (m) { - connection.logdebug(this, `header: ${line}`); - last_header = m[1]; - spamd_response.headers[m[1]] = m[2]; - return; - } - let fold; - if (last_header && (fold = line.match(/^(\s+.*)/))) { - spamd_response.headers[last_header] += `\r\n${fold[1]}`; - return; - } - last_header = ''; - } - }); - - socket.once('end', () => { - if (!connection.transaction) return next() // client gone - - if (spamd_response.headers?.Tests) { - spamd_response.tests = spamd_response.headers.Tests.replace(/\s/g, ''); - } - if (spamd_response.tests === undefined) { - // strip the 'tests' from the X-Spam-Status header - if (spamd_response.headers?.Status) { - // SpamAssassin appears to have a bug that causes a space not to - // be added before autolearn= when the header line has been folded. - // So we modify the regexp here not to match autolearn onwards. - const tests = /tests=((?:(?!autolearn)[^ ])+)/.exec( - spamd_response.headers.Status.replace(/\r?\n\t/g,'') - ); - if (tests) spamd_response.tests = tests[1]; - } - } - - // do stuff with the results... - txn.notes.spamassassin = spamd_response; - connection.results.add(this, { - time: (Date.now() - start)/1000, - hits: spamd_response.hits, - flag: spamd_response.flag, - }); - - this.fixup_old_headers(txn); - this.do_header_updates(connection, spamd_response); - this.log_results(connection, spamd_response); - - const exceeds_err = this.score_too_high(connection, spamd_response); - if (exceeds_err) return next(DENY, exceeds_err); - - this.munge_subject(connection, spamd_response.score); - - next(); - }); -} - -exports.fixup_old_headers = function (txn) { - const action = this.cfg.main.old_headers_action; - const { headers } = txn.notes.spamassassin; - - let key; - switch (action) { - case "keep": - break; - case "drop": - for (key in headers) { - if (!key) continue; - txn.remove_header(`X-Spam-${key}`); - } - break; - // case 'rename': - default: - for (key in headers) { - if (!key) continue; - key = `X-Spam-${key}`; - const old_val = txn.header.get(key); - txn.remove_header(key); - if (old_val) { - // plugin.logdebug(plugin, `header: ${key}, ${old_val}`); - txn.add_header(key.replace(/^X-/, 'X-Old-'), old_val); - } - } - break; - } -} - -exports.munge_subject = function (conn, score) { - const munge = this.cfg.main.munge_subject_threshold; - if (!munge) return; - if (parseFloat(score) < parseFloat(munge)) return; - - const subj = conn.transaction.header.get('Subject'); - const subject_re = new RegExp(`^${utils.regexp_escape(this.cfg.main.subject_prefix)}`); - if (subject_re.test(subj)) return; // prevent double munge - - conn.transaction.remove_header('Subject'); - conn.transaction.add_header('Subject', `${this.cfg.main.subject_prefix} ${subj}`); -} - -exports.do_header_updates = function (conn, spamd_response) { - if (spamd_response.flag === 'Yes') { - // X-Spam-Flag is added by SpamAssassin - conn.transaction.remove_header('precedence'); - conn.transaction.add_header('Precedence', 'junk'); - } - - const modern = this.cfg.main.modern_status_syntax; - if ( !this.cfg.main.add_headers ) return; - - for (const key in spamd_response.headers) { - if (!key || key === '' || key === undefined) continue; - let val = spamd_response.headers[key]; - if (val === undefined) { val = ''; } - - if (key === 'Status' && !modern) { - const legacy = spamd_response.headers[key].replace(/ score=/,' hits='); - conn.transaction.add_header('X-Spam-Status', legacy); - continue; - } - if (val === '') continue; - conn.transaction.add_header(`X-Spam-${key}`, val); - } -} - -exports.score_too_high = function (conn, spamd_response) { - const { score } = spamd_response; - if (conn.relaying) { - const rmax = this.cfg.main.relay_reject_threshold; - if (rmax && (score >= rmax)) { - return "spam score exceeded relay threshold"; - } - } - - const max = this.cfg.main.reject_threshold; - if (max && (score >= max)) return "spam score exceeded threshold"; - - return ''; -} - -exports.get_spamd_username = function (conn) { - - let user = conn.transaction.notes.spamd_user; // 1st priority - if (user && user !== undefined) return user; - - if (!this.cfg.main.spamd_user) return 'default'; // when not defined - user = this.cfg.main.spamd_user; - - // Enable per-user SA prefs - if (user === 'first-recipient') { // special cases - return conn.transaction.rcpt_to[0].address(); - } - if (user === 'all-recipients') { - throw "Unimplemented"; - // TODO: pass the message through SA for each recipient. Then apply - // the least strict result to the connection. That is useful when - // one user blacklists a sender that another user wants to get mail - // from. If this is something you care about, this is the spot. - } - return user; -} - -exports.get_spamd_headers = function (conn, username) { - // http://svn.apache.org/repos/asf/spamassassin/trunk/spamd/PROTOCOL - const headers = [ - 'HEADERS SPAMC/1.4', - `User: ${username}`, - '', - `X-Envelope-From: ${conn.transaction.mail_from.address()}`, - `X-Haraka-UUID: ${conn.transaction.uuid}`, - ]; - if (conn.relaying) { - headers.push(`${this.cfg.main.spamc_auth_header}: true`); - } - - return headers; -} - -exports.get_spamd_socket = function (next, conn, headers) { - const plugin = this; - const txn = conn.transaction; - - const socket = new net.Socket(); - socket.is_connected = false; - net_utils.add_line_processor(socket) - const results_timeout = parseInt(plugin.cfg.main.results_timeout) || 300; - - socket.on('connect', function () { - // Abort if the transaction is gone - if (!txn) { - plugin.logwarn(conn, 'Transaction gone, cancelling SPAMD connection'); - socket.end(); - return; - } - - this.is_connected = true; - // Reset timeout - this.setTimeout(results_timeout * 1000); - socket.write(`${headers.join("\r\n")}\r\n`); - conn.transaction.message_stream.pipe(socket); - }); - - socket.on('error', err => { - socket.destroy(); - if (txn) txn.results.add(plugin, {err: `socket error: ${err.message}` }); - if (plugin.cfg.defer.error) return next(DENYSOFT, 'spamd scan error'); - return next(); - }); - - socket.on('timeout', function () { - socket.destroy(); - if (!this.is_connected) { - if (txn) txn.results.add(plugin, {err: `socket connect timeout` }); - if (plugin.cfg.defer.connect_timeout) return next(DENYSOFT, 'spamd connect timeout'); - } - else { - if (txn) txn.results.add(plugin, {err: `timeout waiting for results` }); - if (plugin.cfg.defer.scan_timeout) return next(DENYSOFT, 'spamd scan timeout'); - } - return next(); - }); - - const connect_timeout = parseInt(plugin.cfg.main.connect_timeout) || 30; - socket.setTimeout(connect_timeout * 1000); - - if (plugin.cfg.main.spamd_socket.match(/\//)) { // assume unix socket - socket.connect(plugin.cfg.main.spamd_socket); - } - else { - const hostport = plugin.cfg.main.spamd_socket.split(/:/); - socket.connect((hostport[1] || 783), hostport[0]); - } - - return socket; -} - -exports.log_results = function (conn, spamd_response) { - const cfg = this.cfg.main; - const reject_threshold = (conn.relaying) ? (cfg.relay_reject_threshold || cfg.reject_threshold) : cfg.reject_threshold; - - const human_text = `status=${spamd_response.flag}` + - `, score=${spamd_response.score}` + - `, required=${spamd_response.reqd}` + - `, reject=${reject_threshold}` + - `, tests="${spamd_response.tests}"`; - - conn.transaction.results.add(this, { - human: human_text, - status: spamd_response.flag, score: parseFloat(spamd_response.score), - required: parseFloat(spamd_response.reqd), reject: reject_threshold, tests: spamd_response.tests, - emit: true}); -} - -exports.should_skip = function (connection = {}) { - const { transaction } = connection; - if (!transaction) return true; - - // a message might be skipped for multiple reasons, store each in results - let result = false; // default - - const max = this.cfg.main.max_size; - if (max) { - const size = connection.transaction.data_bytes; - if (size > max) { - connection.transaction.results.add(this, { skip: `size ${utils.prettySize(size)} exceeds max: ${utils.prettySize(max)}`}); - result = true; - } - } - - if (this.cfg.check.authenticated == false && connection.notes.auth_user) { - connection.transaction.results.add(this, { skip: 'authed'}); - result = true; - } - - if (this.cfg.check.relay == false && connection.relaying) { - connection.transaction.results.add(this, { skip: 'relay'}); - result = true; - } - - if (this.cfg.check.local_ip == false && connection.remote.is_local) { - connection.transaction.results.add(this, { skip: 'local_ip'}); - result = true; - } - - 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 = true; - } - } - - return result; -} diff --git a/test/plugins/spamassassin.js b/test/plugins/spamassassin.js deleted file mode 100644 index ff3191230..000000000 --- a/test/plugins/spamassassin.js +++ /dev/null @@ -1,154 +0,0 @@ -'use strict'; -const assert = require('node:assert') - -const Address = require('address-rfc2821'); -const fixtures = require('haraka-test-fixtures'); - -const _set_up = (done) => { - - this.plugin = new fixtures.plugin('spamassassin'); - this.plugin.cfg = { - main: { - spamc_auth_header: 'X-Haraka-Relaying123' - }, - check: {}, - }; - - this.connection = fixtures.connection.createConnection(); - this.connection.init_transaction() - - done(); -} - -describe('spamassassin', () => { - beforeEach(_set_up) - - describe('register', () => { - - it('loads the spamassassin plugin', () => { - assert.equal('spamassassin', this.plugin.name); - }) - - it('register loads spamassassin.ini', () => { - this.plugin.register(); - assert.ok(this.plugin.cfg); - assert.ok(this.plugin.cfg.main.spamd_socket); - }) - }) - - describe('load_spamassassin_ini', () => { - beforeEach(_set_up) - - it('loads spamassassin.ini', () => { - assert.equal(undefined, this.plugin.cfg.main.spamd_socket); - this.plugin.load_spamassassin_ini(); - assert.ok(this.plugin.cfg.main.spamd_socket); - assert.equal(this.plugin.cfg.main.spamc_auth_header, 'X-Haraka-Relay'); - }) - }) - - describe('should_skip', () => { - - it('max_size not set', () => { - assert.equal(false, this.plugin.should_skip(this.connection)); - }) - - it('max_size 10, data_bytes 9 = false', () => { - this.plugin.cfg.main = { max_size: 10 }; - this.connection.transaction.data_bytes = 9; - assert.equal(false, this.plugin.should_skip(this.connection)); - }) - - it('max_size 10, data_bytes 11 = true', () => { - this.plugin.cfg.main = { max_size: 10 }; - this.connection.transaction.data_bytes = 11; - assert.equal(true, this.plugin.should_skip(this.connection)); - }) - }) - - describe('get_spamd_headers', () => { - - it('returns a spamd protocol request', () => { - this.connection.transaction.mail_from = new Address.Address(''); - this.connection.transaction.uuid = 'THIS-IS-A-TEST-UUID'; - const headers = this.plugin.get_spamd_headers(this.connection, 'test_user'); - const expected_headers = [ - 'HEADERS SPAMC/1.4', - 'User: test_user', - '', - 'X-Envelope-From: matt@example.com', - 'X-Haraka-UUID: THIS-IS-A-TEST-UUID' - ]; - assert.deepEqual(headers, expected_headers); - }) - }) - - describe('get_spamd_headers_relaying', () => { - beforeEach(_set_up) - - it('returns a spamd protocol request when relaying', () => { - this.connection.transaction.mail_from = new Address.Address(''); - this.connection.transaction.uuid = 'THIS-IS-A-TEST-UUID'; - this.connection.set('relaying', true); - const headers = this.plugin.get_spamd_headers(this.connection, 'test_user'); - const expected_headers = [ - 'HEADERS SPAMC/1.4', - 'User: test_user', - '', - 'X-Envelope-From: matt@example.com', - 'X-Haraka-UUID: THIS-IS-A-TEST-UUID', - 'X-Haraka-Relaying123: true', - ]; - assert.deepEqual(headers, expected_headers); - }) - }) - - describe('get_spamd_username', () => { - beforeEach(_set_up) - - it('default', () => { - assert.equal('default', this.plugin.get_spamd_username(this.connection)); - }) - - it('set in txn.notes.spamd_user', () => { - this.connection.transaction.notes.spamd_user = 'txuser'; - assert.equal('txuser', this.plugin.get_spamd_username(this.connection)); - }) - - it('set in cfg.main.spamd_user', () => { - this.plugin.cfg.main.spamd_user = 'cfguser'; - assert.equal('cfguser', this.plugin.get_spamd_username(this.connection)); - }) - - it('set to first-recipient', () => { - this.plugin.cfg.main.spamd_user = 'first-recipient'; - this.connection.transaction.rcpt_to = [ new Address.Address('') ]; - assert.equal('matt@example.com', this.plugin.get_spamd_username(this.connection)); - }) - }) - - describe('score_too_high', () => { - beforeEach(_set_up) - - it('no threshhold is not too high', () => { - assert.ok(!this.plugin.score_too_high(this.connection, {score: 5})); - }) - - it('too high score is too high', () => { - this.plugin.cfg.main.reject_threshold = 5; - assert.equal('spam score exceeded threshold', this.plugin.score_too_high(this.connection, {score: 6})); - }) - - it('ok score with relaying is ok', () => { - this.connection.relaying = true; - this.plugin.cfg.main.relay_reject_threshold = 7; - assert.equal('', this.plugin.score_too_high(this.connection, {score: 6})); - }) - - it('too high score with relaying is too high', () => { - this.connection.relaying = true; - this.plugin.cfg.main.relay_reject_threshold = 7; - assert.equal('spam score exceeded relay threshold', this.plugin.score_too_high(this.connection, {score: 8})); - }) - }) -})