Haraka 3.1.6 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -1
- package/CONTRIBUTORS.md +8 -8
- package/Plugins.md +99 -99
- package/address.js +53 -0
- package/bin/haraka +1 -1
- package/config/smtp_forward.ini +10 -0
- package/config/smtp_proxy.ini +10 -0
- package/connection.js +28 -11
- package/docs/Outbound.md +1 -1
- package/docs/Transaction.md +1 -1
- package/docs/plugins/queue/smtp_forward.md +19 -3
- package/docs/plugins/queue/smtp_proxy.md +10 -2
- package/docs/plugins/status.md +21 -5
- package/haraka.js +1 -1
- package/outbound/hmail.js +41 -41
- package/outbound/index.js +5 -5
- package/outbound/queue.js +1 -1
- package/outbound/tls.js +2 -43
- package/package.json +48 -48
- package/plugins/auth/auth_base.js +9 -3
- package/plugins/auth/auth_proxy.js +14 -11
- package/plugins/block_me.js +6 -4
- package/plugins/prevent_credential_leaks.js +3 -1
- package/plugins/process_title.js +6 -6
- package/plugins/queue/qmail-queue.js +15 -19
- package/plugins/queue/smtp_forward.js +14 -6
- package/plugins/queue/smtp_proxy.js +14 -3
- package/plugins/rcpt_to.host_list_base.js +1 -1
- package/plugins/record_envelope_addresses.js +2 -2
- package/plugins/status.js +34 -5
- package/plugins/tls.js +13 -5
- package/plugins/xclient.js +3 -1
- package/server.js +5 -3
- package/smtp_client.js +20 -11
- package/test/config/block_me.recipient +1 -0
- package/test/config/block_me.senders +1 -0
- package/test/connection.js +25 -1
- package/test/fixtures/util_hmailitem.js +1 -1
- package/test/outbound/bounce_net_errors.js +3 -2
- package/test/outbound/index.js +2 -2
- package/test/plugins/auth/auth_base.js +1 -1
- package/test/plugins/auth/auth_bridge.js +80 -0
- package/test/plugins/auth/flat_file.js +128 -0
- package/test/plugins/block_me.js +157 -0
- package/test/plugins/data.signatures.js +114 -0
- package/test/plugins/delay_deny.js +263 -0
- package/test/plugins/prevent_credential_leaks.js +178 -0
- package/test/plugins/process_title.js +135 -0
- package/test/plugins/queue/deliver.js +99 -0
- package/test/plugins/queue/discard.js +79 -0
- package/test/plugins/queue/lmtp.js +138 -0
- package/test/plugins/queue/qmail-queue.js +99 -0
- package/test/plugins/queue/quarantine.js +81 -0
- package/test/plugins/queue/smtp_bridge.js +154 -0
- package/test/plugins/queue/smtp_forward.js +43 -7
- package/test/plugins/queue/smtp_proxy.js +139 -0
- package/test/plugins/rcpt_to.host_list_base.js +1 -1
- package/test/plugins/rcpt_to.in_host_list.js +1 -1
- package/test/plugins/record_envelope_addresses.js +2 -2
- package/test/plugins/reseed_rng.js +34 -0
- package/test/plugins/status.js +71 -0
- package/test/plugins/tarpit.js +91 -0
- package/test/plugins/tls.js +25 -0
- package/test/plugins/toobusy.js +21 -0
- package/test/plugins/xclient.js +14 -0
- package/test/server.js +59 -0
- package/test/smtp_client.js +46 -13
- package/test/tls_socket.js +82 -0
- package/tls_socket.js +50 -0
package/smtp_client.js
CHANGED
|
@@ -192,7 +192,7 @@ class SMTPClient extends events.EventEmitter {
|
|
|
192
192
|
client.socket.on('end', closed('ended'))
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
-
|
|
195
|
+
load_tls_options(opts = {}) {
|
|
196
196
|
this.tls_options = { servername: this.host, ...opts }
|
|
197
197
|
}
|
|
198
198
|
|
|
@@ -312,8 +312,8 @@ exports.onCapabilitiesOutbound = (smtp_client, secured, connection, config, on_s
|
|
|
312
312
|
// Check if there are any banned TLS hosts
|
|
313
313
|
if (smtp_client.tls_options.no_tls_hosts) {
|
|
314
314
|
// If there are check if these hosts are in the blacklist
|
|
315
|
-
hostBanned = net_utils.ip_in_list(smtp_client.
|
|
316
|
-
serverBanned = net_utils.ip_in_list(smtp_client.
|
|
315
|
+
hostBanned = net_utils.ip_in_list(smtp_client.tls_options.no_tls_hosts, config.host)
|
|
316
|
+
serverBanned = net_utils.ip_in_list(smtp_client.tls_options.no_tls_hosts, smtp_client.remote_ip)
|
|
317
317
|
}
|
|
318
318
|
|
|
319
319
|
if (!hostBanned && !serverBanned && config.enable_tls) {
|
|
@@ -358,7 +358,7 @@ exports.get_client_plugin = (plugin, connection, c, callback) => {
|
|
|
358
358
|
|
|
359
359
|
let secured = false
|
|
360
360
|
|
|
361
|
-
smtp_client.
|
|
361
|
+
smtp_client.load_tls_options(plugin.tls_options)
|
|
362
362
|
|
|
363
363
|
smtp_client.call_next = function (retval, msg) {
|
|
364
364
|
if (this.next) {
|
|
@@ -369,7 +369,10 @@ exports.get_client_plugin = (plugin, connection, c, callback) => {
|
|
|
369
369
|
}
|
|
370
370
|
|
|
371
371
|
smtp_client.on('client_protocol', (line) => {
|
|
372
|
-
|
|
372
|
+
// Don't leak SASL credentials (e.g. AUTH PLAIN <base64>) into
|
|
373
|
+
// protocol logs.
|
|
374
|
+
const safe = String(line).replace(/^(AUTH\s+\S+\s+).+$/i, '$1[redacted]')
|
|
375
|
+
connection.logprotocol(plugin, `C: ${safe}`)
|
|
373
376
|
})
|
|
374
377
|
|
|
375
378
|
smtp_client.on('server_protocol', (line) => {
|
|
@@ -401,21 +404,27 @@ exports.get_client_plugin = (plugin, connection, c, callback) => {
|
|
|
401
404
|
if (!c.auth || smtp_client.authenticated) {
|
|
402
405
|
if (smtp_client.is_dead_sender(plugin, connection)) return
|
|
403
406
|
|
|
404
|
-
smtp_client.send_command('MAIL', `FROM:${connection.transaction.mail_from.format(!smtp_client.
|
|
407
|
+
smtp_client.send_command('MAIL', `FROM:${connection.transaction.mail_from.format(!smtp_client.smtputf8)}`)
|
|
405
408
|
return
|
|
406
409
|
}
|
|
407
410
|
|
|
408
411
|
if (c.auth.type === null || typeof c.auth.type === 'undefined') return // Ignore blank
|
|
409
412
|
const auth_type = c.auth.type.toLowerCase()
|
|
413
|
+
// This listener runs from the socket line handler; an uncaught
|
|
414
|
+
// throw here crashes the forwarding worker. Route failures
|
|
415
|
+
// through the existing smtp_client 'error' flow (logwarn +
|
|
416
|
+
// call_next) so a misconfigured/hostile upstream degrades to a
|
|
417
|
+
// normal SMTP error path instead.
|
|
410
418
|
if (!smtp_client.auth_capabilities.includes(auth_type)) {
|
|
411
|
-
|
|
419
|
+
return smtp_client.emit(
|
|
420
|
+
'error',
|
|
412
421
|
`Auth type "${auth_type}" not supported by server (supports: ${smtp_client.auth_capabilities.join(',')})`,
|
|
413
422
|
)
|
|
414
423
|
}
|
|
415
424
|
switch (auth_type) {
|
|
416
425
|
case 'plain':
|
|
417
426
|
if (!c.auth.user || !c.auth.pass) {
|
|
418
|
-
|
|
427
|
+
return smtp_client.emit('error', 'Must include auth.user and auth.pass for PLAIN auth.')
|
|
419
428
|
}
|
|
420
429
|
logger.debug(`[smtp_client] uuid=${smtp_client.uuid} authenticating as "${c.auth.user}"`)
|
|
421
430
|
smtp_client.send_command(
|
|
@@ -424,9 +433,9 @@ exports.get_client_plugin = (plugin, connection, c, callback) => {
|
|
|
424
433
|
)
|
|
425
434
|
break
|
|
426
435
|
case 'cram-md5':
|
|
427
|
-
|
|
436
|
+
return smtp_client.emit('error', `AUTH ${auth_type} not implemented`)
|
|
428
437
|
default:
|
|
429
|
-
|
|
438
|
+
return smtp_client.emit('error', `Unknown AUTH type: ${auth_type}`)
|
|
430
439
|
}
|
|
431
440
|
})
|
|
432
441
|
|
|
@@ -437,7 +446,7 @@ exports.get_client_plugin = (plugin, connection, c, callback) => {
|
|
|
437
446
|
if (smtp_client.is_dead_sender(plugin, connection)) return
|
|
438
447
|
|
|
439
448
|
smtp_client.authenticated = true
|
|
440
|
-
smtp_client.send_command('MAIL', `FROM:${connection.transaction.mail_from.format(!smtp_client.
|
|
449
|
+
smtp_client.send_command('MAIL', `FROM:${connection.transaction.mail_from.format(!smtp_client.smtputf8)}`)
|
|
441
450
|
})
|
|
442
451
|
|
|
443
452
|
// these errors only get thrown when the connection is still active
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
blocklist@example.com
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sender@example.com
|
package/test/connection.js
CHANGED
|
@@ -5,7 +5,7 @@ const assert = require('node:assert/strict')
|
|
|
5
5
|
|
|
6
6
|
const constants = require('haraka-constants')
|
|
7
7
|
const DSN = require('haraka-dsn')
|
|
8
|
-
const { Address } = require('address
|
|
8
|
+
const { Address } = require('../address')
|
|
9
9
|
|
|
10
10
|
const connection = require('../connection')
|
|
11
11
|
const Server = require('../server')
|
|
@@ -676,4 +676,28 @@ describe('connection', () => {
|
|
|
676
676
|
assert.equal(this.connection.transaction.data_bytes, 0)
|
|
677
677
|
})
|
|
678
678
|
})
|
|
679
|
+
|
|
680
|
+
describe('header injection', () => {
|
|
681
|
+
beforeEach(setUp)
|
|
682
|
+
|
|
683
|
+
it('cmd_helo rejects control chars in the host', () => {
|
|
684
|
+
const r = this.connection.cmd_helo('evil\rINJECTED')
|
|
685
|
+
assert.match(String(r), /^501/)
|
|
686
|
+
assert.equal(this.connection.hello.host, null)
|
|
687
|
+
})
|
|
688
|
+
|
|
689
|
+
it('cmd_ehlo rejects control chars in the host', () => {
|
|
690
|
+
const r = this.connection.cmd_ehlo('evil\r\nINJECTED')
|
|
691
|
+
assert.match(String(r), /^501/)
|
|
692
|
+
assert.equal(this.connection.hello.host, null)
|
|
693
|
+
})
|
|
694
|
+
|
|
695
|
+
it('auth_results strips CR/LF so it cannot inject a header', () => {
|
|
696
|
+
const out = this.connection.auth_results('auth=fail smtp.auth=evil\r\nInjected-Header: pwned')
|
|
697
|
+
assert.ok(!out.includes('\r\nInjected-Header:'))
|
|
698
|
+
// the only CRLF present is the legitimate folding (;\r\n\t)
|
|
699
|
+
assert.equal(out.replace(/;\r\n\t/g, '').includes('\r'), false)
|
|
700
|
+
assert.equal(out.replace(/;\r\n\t/g, '').includes('\n'), false)
|
|
701
|
+
})
|
|
702
|
+
})
|
|
679
703
|
})
|
|
@@ -98,9 +98,10 @@ const testCases = [
|
|
|
98
98
|
trigger: (h) => HMailItem.prototype.get_mx_error.apply(h, [{ code: 'SOME-OTHER-ERR' }, {}]),
|
|
99
99
|
},
|
|
100
100
|
{
|
|
101
|
-
|
|
101
|
+
// RFC 7505 NULL MX → DSN.addr_null_mx → 5.1.10 (per haraka-dsn).
|
|
102
|
+
name: 'found_mx with NULL MX (RFC 7505) triggers bounce with dsn_status 5.1.10',
|
|
102
103
|
method: 'bounce',
|
|
103
|
-
status: '5.1.
|
|
104
|
+
status: '5.1.10',
|
|
104
105
|
trigger: (h) => HMailItem.prototype.found_mx.apply(h, [[{ priority: 0, exchange: '' }]]),
|
|
105
106
|
},
|
|
106
107
|
{
|
package/test/outbound/index.js
CHANGED
|
@@ -211,7 +211,7 @@ describe('outbound', () => {
|
|
|
211
211
|
it('yields to setImmediate before opening process_delivery pipes', async () => {
|
|
212
212
|
const stream = require('node:stream')
|
|
213
213
|
const Transaction = require('../../transaction')
|
|
214
|
-
const Address = require('address
|
|
214
|
+
const Address = require('../../address').Address
|
|
215
215
|
const outbound = require('../../outbound')
|
|
216
216
|
const plugins = require('../../plugins')
|
|
217
217
|
|
|
@@ -271,7 +271,7 @@ describe('outbound', () => {
|
|
|
271
271
|
|
|
272
272
|
it('adds missing Message-Id/Date and prepends Received before queueing', async () => {
|
|
273
273
|
process.env.HARAKA_TEST_DIR = path.resolve('test')
|
|
274
|
-
const Address = require('address
|
|
274
|
+
const Address = require('../../address').Address
|
|
275
275
|
const outbound = require('../../outbound')
|
|
276
276
|
const plugins = require('../../plugins')
|
|
277
277
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
const assert = require('node:assert')
|
|
3
3
|
const { describe, it, beforeEach } = require('node:test')
|
|
4
4
|
|
|
5
|
-
const { Address } = require('address
|
|
5
|
+
const { Address } = require('../../../address')
|
|
6
6
|
const fixtures = require('haraka-test-fixtures')
|
|
7
7
|
const utils = require('haraka-utils')
|
|
8
8
|
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert')
|
|
4
|
+
const { describe, it, beforeEach } = require('node:test')
|
|
5
|
+
|
|
6
|
+
const fixtures = require('haraka-test-fixtures')
|
|
7
|
+
|
|
8
|
+
describe('auth/auth_bridge', () => {
|
|
9
|
+
let plugin
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
plugin = new fixtures.plugin('auth/auth_bridge')
|
|
13
|
+
plugin.load_flat_ini()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
describe('load_flat_ini', () => {
|
|
17
|
+
it('loads smtp_bridge.ini config', () => {
|
|
18
|
+
assert.ok(plugin.cfg)
|
|
19
|
+
assert.ok(plugin.cfg.main)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('cfg.main.host defaults to localhost', () => {
|
|
23
|
+
assert.equal(plugin.cfg.main.host, 'localhost')
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('check_plain_passwd', () => {
|
|
28
|
+
let conn
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
conn = fixtures.connection.createConnection()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('calls try_auth_proxy with just host when no port configured', (t, done) => {
|
|
35
|
+
plugin.cfg.main = { host: 'mail.example.com' }
|
|
36
|
+
plugin.try_auth_proxy = (connection, host, user, passwd, cb) => {
|
|
37
|
+
assert.equal(host, 'mail.example.com')
|
|
38
|
+
assert.equal(user, 'testuser')
|
|
39
|
+
assert.equal(passwd, 'testpass')
|
|
40
|
+
cb(true)
|
|
41
|
+
}
|
|
42
|
+
plugin.check_plain_passwd(conn, 'testuser', 'testpass', (result) => {
|
|
43
|
+
assert.equal(result, true)
|
|
44
|
+
done()
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('calls try_auth_proxy with host:port when port is configured', (t, done) => {
|
|
49
|
+
plugin.cfg.main = { host: 'mail.example.com', port: '587' }
|
|
50
|
+
plugin.try_auth_proxy = (connection, host, user, passwd, cb) => {
|
|
51
|
+
assert.equal(host, 'mail.example.com:587')
|
|
52
|
+
cb(true)
|
|
53
|
+
}
|
|
54
|
+
plugin.check_plain_passwd(conn, 'testuser', 'testpass', (result) => {
|
|
55
|
+
assert.equal(result, true)
|
|
56
|
+
done()
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('passes authentication failure through to callback', (t, done) => {
|
|
61
|
+
plugin.cfg.main = { host: 'mail.example.com' }
|
|
62
|
+
plugin.try_auth_proxy = (connection, host, user, passwd, cb) => {
|
|
63
|
+
cb(false)
|
|
64
|
+
}
|
|
65
|
+
plugin.check_plain_passwd(conn, 'baduser', 'badpass', (result) => {
|
|
66
|
+
assert.equal(result, false)
|
|
67
|
+
done()
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('passes the connection object to try_auth_proxy', (t, done) => {
|
|
72
|
+
plugin.cfg.main = { host: 'mail.example.com' }
|
|
73
|
+
plugin.try_auth_proxy = (connection, host, user, passwd, cb) => {
|
|
74
|
+
assert.equal(connection, conn)
|
|
75
|
+
cb(true)
|
|
76
|
+
}
|
|
77
|
+
plugin.check_plain_passwd(conn, 'user', 'pass', () => done())
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
})
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert')
|
|
4
|
+
const { describe, it, beforeEach } = require('node:test')
|
|
5
|
+
|
|
6
|
+
const fixtures = require('haraka-test-fixtures')
|
|
7
|
+
|
|
8
|
+
function makeConnection(opts = {}) {
|
|
9
|
+
const conn = fixtures.connection.createConnection()
|
|
10
|
+
conn.capabilities = []
|
|
11
|
+
conn.notes.allowed_auth_methods = []
|
|
12
|
+
conn.remote = { is_private: opts.is_private ?? false }
|
|
13
|
+
conn.tls = { enabled: opts.tls_enabled ?? false }
|
|
14
|
+
return conn
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('auth/flat_file', () => {
|
|
18
|
+
let plugin
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
plugin = new fixtures.plugin('auth/flat_file')
|
|
22
|
+
plugin.inherits('auth/auth_base')
|
|
23
|
+
plugin.load_flat_ini()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('load_flat_ini', () => {
|
|
27
|
+
it('populates cfg.users as an object', () => {
|
|
28
|
+
assert.ok(typeof plugin.cfg.users === 'object')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('cfg.users defaults to empty object when not configured', () => {
|
|
32
|
+
// default config has no real users
|
|
33
|
+
assert.deepEqual(plugin.cfg.users, {})
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('hook_capabilities', () => {
|
|
38
|
+
let conn
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
conn = makeConnection()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('skips for public non-TLS connection', (t, done) => {
|
|
45
|
+
plugin.hook_capabilities((rc) => {
|
|
46
|
+
assert.equal(rc, undefined)
|
|
47
|
+
assert.equal(conn.capabilities.length, 0)
|
|
48
|
+
done()
|
|
49
|
+
}, conn)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('adds AUTH methods for private connection (non-TLS)', (t, done) => {
|
|
53
|
+
conn.remote.is_private = true
|
|
54
|
+
plugin.cfg.core.methods = 'PLAIN,LOGIN'
|
|
55
|
+
plugin.hook_capabilities((rc) => {
|
|
56
|
+
assert.equal(rc, undefined)
|
|
57
|
+
assert.ok(
|
|
58
|
+
conn.capabilities.some((c) => c.startsWith('AUTH ')),
|
|
59
|
+
'AUTH capability should be present',
|
|
60
|
+
)
|
|
61
|
+
done()
|
|
62
|
+
}, conn)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('adds AUTH methods when TLS is enabled', (t, done) => {
|
|
66
|
+
conn.tls.enabled = true
|
|
67
|
+
plugin.cfg.core.methods = 'PLAIN,LOGIN'
|
|
68
|
+
plugin.hook_capabilities((rc) => {
|
|
69
|
+
assert.equal(rc, undefined)
|
|
70
|
+
assert.ok(conn.capabilities.some((c) => c.startsWith('AUTH ')))
|
|
71
|
+
done()
|
|
72
|
+
}, conn)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('sets allowed_auth_methods on connection notes', (t, done) => {
|
|
76
|
+
conn.tls.enabled = true
|
|
77
|
+
plugin.cfg.core.methods = 'PLAIN,LOGIN'
|
|
78
|
+
plugin.hook_capabilities(() => {
|
|
79
|
+
assert.deepEqual(conn.notes.allowed_auth_methods, ['PLAIN', 'LOGIN'])
|
|
80
|
+
done()
|
|
81
|
+
}, conn)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('does not add AUTH when no methods configured', (t, done) => {
|
|
85
|
+
conn.tls.enabled = true
|
|
86
|
+
plugin.cfg.core.methods = null
|
|
87
|
+
plugin.hook_capabilities(() => {
|
|
88
|
+
assert.equal(conn.capabilities.length, 0)
|
|
89
|
+
done()
|
|
90
|
+
}, conn)
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
describe('get_plain_passwd', () => {
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
plugin.cfg.users = { alice: 'secret', bob: 'hunter2' }
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('returns password for known user', (t, done) => {
|
|
100
|
+
plugin.get_plain_passwd('alice', {}, (pw) => {
|
|
101
|
+
assert.equal(pw, 'secret')
|
|
102
|
+
done()
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('calls cb with no args for unknown user', (t, done) => {
|
|
107
|
+
plugin.get_plain_passwd('unknown', {}, (pw) => {
|
|
108
|
+
assert.equal(pw, undefined)
|
|
109
|
+
done()
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('handles multiple users', (t, done) => {
|
|
114
|
+
plugin.get_plain_passwd('bob', {}, (pw) => {
|
|
115
|
+
assert.equal(pw, 'hunter2')
|
|
116
|
+
done()
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('coerces password to string via toString()', (t, done) => {
|
|
121
|
+
plugin.cfg.users.numericuser = 12345
|
|
122
|
+
plugin.get_plain_passwd('numericuser', {}, (pw) => {
|
|
123
|
+
assert.equal(pw, '12345')
|
|
124
|
+
done()
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
})
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs')
|
|
4
|
+
const path = require('node:path')
|
|
5
|
+
const assert = require('node:assert/strict')
|
|
6
|
+
const { describe, it, beforeEach, after } = require('node:test')
|
|
7
|
+
|
|
8
|
+
const fixtures = require('haraka-test-fixtures')
|
|
9
|
+
const { Address } = require('@haraka/email-address')
|
|
10
|
+
require('haraka-constants').import(global)
|
|
11
|
+
|
|
12
|
+
// block_me appends to <config>/mail_from.blocklist when a sender is blocked;
|
|
13
|
+
// remove the artifact the 'sets block_me note' test produces.
|
|
14
|
+
after(() => {
|
|
15
|
+
fs.rmSync(path.resolve('test/config/mail_from.blocklist'), { force: true })
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
function makeConnection({
|
|
19
|
+
relaying = false,
|
|
20
|
+
mailFrom = 'sender@example.com',
|
|
21
|
+
rcptTo = ['blocklist@example.com'],
|
|
22
|
+
} = {}) {
|
|
23
|
+
const conn = fixtures.connection.createConnection()
|
|
24
|
+
conn.init_transaction()
|
|
25
|
+
conn.relaying = relaying
|
|
26
|
+
conn.transaction.mail_from = new Address(`<${mailFrom}>`)
|
|
27
|
+
conn.transaction.rcpt_to = rcptTo.map((r) => new Address(`<${r}>`))
|
|
28
|
+
conn.transaction.body = { bodytext: '', children: [] }
|
|
29
|
+
return conn
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('block_me', () => {
|
|
33
|
+
let plugin
|
|
34
|
+
|
|
35
|
+
// Read config (block_me.recipient, block_me.senders) from test/config rather
|
|
36
|
+
// than the real config dir. block_me also appends matched senders to
|
|
37
|
+
// mail_from.blocklist; with this override that write lands in test/config too.
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
plugin = new fixtures.plugin('block_me')
|
|
40
|
+
plugin.config = plugin.config.module_config(path.resolve('test'))
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe('hook_data', () => {
|
|
44
|
+
it('enables body parsing and calls next', (t, done) => {
|
|
45
|
+
const conn = makeConnection()
|
|
46
|
+
conn.transaction.parse_body = false
|
|
47
|
+
plugin.hook_data((rc) => {
|
|
48
|
+
assert.equal(rc, undefined)
|
|
49
|
+
assert.equal(conn.transaction.parse_body, true)
|
|
50
|
+
done()
|
|
51
|
+
}, conn)
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
describe('hook_data_post', () => {
|
|
56
|
+
it('calls next when not relaying', (t, done) => {
|
|
57
|
+
const conn = makeConnection({ relaying: false })
|
|
58
|
+
plugin.hook_data_post((rc) => {
|
|
59
|
+
assert.equal(rc, undefined)
|
|
60
|
+
done()
|
|
61
|
+
}, conn)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('calls next when transaction is missing', (t, done) => {
|
|
65
|
+
const conn = fixtures.connection.createConnection()
|
|
66
|
+
conn.relaying = true
|
|
67
|
+
conn.transaction = null
|
|
68
|
+
plugin.hook_data_post((rc) => {
|
|
69
|
+
assert.equal(rc, undefined)
|
|
70
|
+
done()
|
|
71
|
+
}, conn)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('calls next when more than one recipient', (t, done) => {
|
|
75
|
+
const conn = makeConnection({
|
|
76
|
+
relaying: true,
|
|
77
|
+
rcptTo: ['blocklist@example.com', 'other@example.com'],
|
|
78
|
+
})
|
|
79
|
+
plugin.hook_data_post((rc) => {
|
|
80
|
+
assert.equal(rc, undefined)
|
|
81
|
+
done()
|
|
82
|
+
}, conn)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('calls next when recipient does not match configured address', (t, done) => {
|
|
86
|
+
const conn = makeConnection({ relaying: true, rcptTo: ['other@example.com'] })
|
|
87
|
+
plugin.hook_data_post((rc) => {
|
|
88
|
+
assert.equal(rc, undefined)
|
|
89
|
+
done()
|
|
90
|
+
}, conn)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('denies when sender is not in the allowed senders list', (t, done) => {
|
|
94
|
+
const conn = makeConnection({
|
|
95
|
+
relaying: true,
|
|
96
|
+
mailFrom: 'notallowed@example.com',
|
|
97
|
+
rcptTo: ['blocklist@example.com'],
|
|
98
|
+
})
|
|
99
|
+
plugin.hook_data_post((rc, msg) => {
|
|
100
|
+
assert.equal(rc, DENY)
|
|
101
|
+
assert.ok(msg.includes('not allowed'))
|
|
102
|
+
done()
|
|
103
|
+
}, conn)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('calls next when no From header found in body', (t, done) => {
|
|
107
|
+
const conn = makeConnection({
|
|
108
|
+
relaying: true,
|
|
109
|
+
mailFrom: 'sender@example.com',
|
|
110
|
+
rcptTo: ['blocklist@example.com'],
|
|
111
|
+
})
|
|
112
|
+
conn.transaction.body = { bodytext: 'No from header here', children: [] }
|
|
113
|
+
plugin.hook_data_post((rc) => {
|
|
114
|
+
assert.equal(rc, undefined)
|
|
115
|
+
// note should not be set since no From header
|
|
116
|
+
assert.equal(conn.transaction.notes.block_me, undefined)
|
|
117
|
+
done()
|
|
118
|
+
}, conn)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('sets block_me note and calls next when From is extracted', (t, done) => {
|
|
122
|
+
const conn = makeConnection({
|
|
123
|
+
relaying: true,
|
|
124
|
+
mailFrom: 'sender@example.com',
|
|
125
|
+
rcptTo: ['blocklist@example.com'],
|
|
126
|
+
})
|
|
127
|
+
conn.transaction.body = {
|
|
128
|
+
bodytext: 'From: Test User <block_target@example.com>',
|
|
129
|
+
children: [],
|
|
130
|
+
}
|
|
131
|
+
plugin.hook_data_post((rc) => {
|
|
132
|
+
assert.equal(rc, undefined)
|
|
133
|
+
assert.equal(conn.transaction.notes.block_me, 1)
|
|
134
|
+
done()
|
|
135
|
+
}, conn)
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
describe('hook_queue', () => {
|
|
140
|
+
it('returns OK when block_me note is set on transaction', (t, done) => {
|
|
141
|
+
const conn = makeConnection()
|
|
142
|
+
conn.transaction.notes.block_me = 1
|
|
143
|
+
plugin.hook_queue((rc) => {
|
|
144
|
+
assert.equal(rc, OK)
|
|
145
|
+
done()
|
|
146
|
+
}, conn)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('calls next when block_me note is not set', (t, done) => {
|
|
150
|
+
const conn = makeConnection()
|
|
151
|
+
plugin.hook_queue((rc) => {
|
|
152
|
+
assert.equal(rc, undefined)
|
|
153
|
+
done()
|
|
154
|
+
}, conn)
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
})
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict')
|
|
4
|
+
const { describe, it, beforeEach } = require('node:test')
|
|
5
|
+
|
|
6
|
+
const fixtures = require('haraka-test-fixtures')
|
|
7
|
+
require('haraka-constants').import(global)
|
|
8
|
+
|
|
9
|
+
function makeConnection(bodytext = '', children = []) {
|
|
10
|
+
const conn = fixtures.connection.createConnection()
|
|
11
|
+
conn.init_transaction()
|
|
12
|
+
conn.transaction.body = { bodytext, children }
|
|
13
|
+
return conn
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('data.signatures', () => {
|
|
17
|
+
let plugin
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
plugin = new fixtures.plugin('data.signatures')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('hook_data', () => {
|
|
24
|
+
it('enables body parsing', (t, done) => {
|
|
25
|
+
const conn = makeConnection()
|
|
26
|
+
conn.transaction.parse_body = false
|
|
27
|
+
plugin.hook_data((rc) => {
|
|
28
|
+
assert.equal(rc, undefined)
|
|
29
|
+
assert.equal(conn.transaction.parse_body, true)
|
|
30
|
+
done()
|
|
31
|
+
}, conn)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('calls next when there is no transaction', (t, done) => {
|
|
35
|
+
const conn = fixtures.connection.createConnection()
|
|
36
|
+
conn.transaction = null
|
|
37
|
+
plugin.hook_data((rc) => {
|
|
38
|
+
assert.equal(rc, undefined)
|
|
39
|
+
done()
|
|
40
|
+
}, conn)
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe('hook_data_post', () => {
|
|
45
|
+
it('calls next when there is no transaction', (t, done) => {
|
|
46
|
+
const conn = fixtures.connection.createConnection()
|
|
47
|
+
conn.transaction = null
|
|
48
|
+
plugin.hook_data_post((rc) => {
|
|
49
|
+
assert.equal(rc, undefined)
|
|
50
|
+
done()
|
|
51
|
+
}, conn)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('calls next when signature list is empty', (t, done) => {
|
|
55
|
+
plugin.config.get = (name, type) => (type === 'list' ? [] : {})
|
|
56
|
+
const conn = makeConnection('This is some email body text')
|
|
57
|
+
plugin.hook_data_post((rc) => {
|
|
58
|
+
assert.equal(rc, undefined)
|
|
59
|
+
done()
|
|
60
|
+
}, conn)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('denies when body matches a signature', (t, done) => {
|
|
64
|
+
plugin.config.get = (name, type) => (type === 'list' ? ['spam_signature_text'] : {})
|
|
65
|
+
const conn = makeConnection('Buy cheap meds! spam_signature_text here')
|
|
66
|
+
plugin.hook_data_post((rc, msg) => {
|
|
67
|
+
assert.equal(rc, DENY)
|
|
68
|
+
assert.ok(msg.includes('spam'))
|
|
69
|
+
done()
|
|
70
|
+
}, conn)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('calls next when body does not match any signature', (t, done) => {
|
|
74
|
+
plugin.config.get = (name, type) => (type === 'list' ? ['bad_pattern'] : {})
|
|
75
|
+
const conn = makeConnection('Totally normal email body')
|
|
76
|
+
plugin.hook_data_post((rc) => {
|
|
77
|
+
assert.equal(rc, undefined)
|
|
78
|
+
done()
|
|
79
|
+
}, conn)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('denies when a child body part matches a signature', (t, done) => {
|
|
83
|
+
plugin.config.get = (name, type) => (type === 'list' ? ['spam_in_child'] : {})
|
|
84
|
+
const conn = fixtures.connection.createConnection()
|
|
85
|
+
conn.init_transaction()
|
|
86
|
+
conn.transaction.body = {
|
|
87
|
+
bodytext: 'clean parent text',
|
|
88
|
+
children: [{ bodytext: 'spam_in_child content here', children: [] }],
|
|
89
|
+
}
|
|
90
|
+
plugin.hook_data_post((rc, msg) => {
|
|
91
|
+
assert.equal(rc, DENY)
|
|
92
|
+
done()
|
|
93
|
+
}, conn)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('calls next when multiple signatures do not match', (t, done) => {
|
|
97
|
+
plugin.config.get = (name, type) => (type === 'list' ? ['sig_one', 'sig_two', 'sig_three'] : {})
|
|
98
|
+
const conn = makeConnection('No matching signatures here at all')
|
|
99
|
+
plugin.hook_data_post((rc) => {
|
|
100
|
+
assert.equal(rc, undefined)
|
|
101
|
+
done()
|
|
102
|
+
}, conn)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('matches the first of multiple signatures', (t, done) => {
|
|
106
|
+
plugin.config.get = (name, type) => (type === 'list' ? ['no_match', 'buy_cheap_pills'] : {})
|
|
107
|
+
const conn = makeConnection('This message has buy_cheap_pills for you')
|
|
108
|
+
plugin.hook_data_post((rc) => {
|
|
109
|
+
assert.equal(rc, DENY)
|
|
110
|
+
done()
|
|
111
|
+
}, conn)
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
})
|