Haraka 3.1.3 → 3.1.5

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.
Files changed (65) hide show
  1. package/.prettierignore +2 -0
  2. package/CONTRIBUTORS.md +23 -1
  3. package/Changes.md +52 -0
  4. package/Plugins.md +81 -64
  5. package/README.md +1 -1
  6. package/bin/haraka +7 -5
  7. package/connection.js +15 -19
  8. package/docs/Plugins.md +1 -1
  9. package/docs/plugins/aliases.md +0 -2
  10. package/docs/plugins/queue/qmail-queue.md +0 -1
  11. package/logger.js +2 -2
  12. package/outbound/hmail.js +76 -83
  13. package/outbound/index.js +36 -34
  14. package/outbound/queue.js +231 -176
  15. package/package.json +26 -29
  16. package/plugins/prevent_credential_leaks.js +2 -2
  17. package/plugins/process_title.js +1 -1
  18. package/plugins/queue/smtp_forward.js +5 -5
  19. package/plugins/status.js +8 -5
  20. package/plugins/tls.js +1 -1
  21. package/plugins.js +19 -14
  22. package/rfc1869.js +10 -10
  23. package/run_tests +8 -2
  24. package/server.js +15 -10
  25. package/smtp_client.js +10 -15
  26. package/test/config/tls/haraka.local.pem +47 -47
  27. package/test/connection.js +286 -147
  28. package/test/endpoint.js +5 -4
  29. package/test/fixtures/line_socket.js +1 -0
  30. package/test/fixtures/util_hmailitem.js +1 -1
  31. package/test/host_pool.js +57 -31
  32. package/test/logger.js +75 -135
  33. package/test/outbound/bounce_net_errors.js +132 -0
  34. package/test/outbound/bounce_rfc3464.js +226 -0
  35. package/test/outbound/hmail.js +140 -104
  36. package/test/outbound/index.js +61 -101
  37. package/test/outbound/qfile.js +25 -25
  38. package/test/outbound/queue.js +233 -0
  39. package/test/plugins/auth/auth_base.js +39 -44
  40. package/test/plugins/auth/auth_vpopmaild.js +8 -9
  41. package/test/plugins/queue/smtp_forward.js +953 -183
  42. package/test/plugins/rcpt_to.host_list_base.js +58 -93
  43. package/test/plugins/rcpt_to.in_host_list.js +126 -175
  44. package/test/plugins/record_envelope_addresses.js +93 -0
  45. package/test/plugins/status.js +10 -10
  46. package/test/plugins/tls.js +11 -21
  47. package/test/plugins/xclient.js +102 -0
  48. package/test/plugins.js +10 -13
  49. package/test/rfc1869.js +71 -48
  50. package/test/server.js +281 -436
  51. package/test/smtp_client.js +1194 -220
  52. package/test/tls_socket.js +74 -243
  53. package/test/transaction.js +486 -201
  54. package/tls_socket.js +19 -23
  55. package/transaction.js +33 -10
  56. package/config/rabbitmq.ini +0 -10
  57. package/config/rabbitmq_amqplib.ini +0 -19
  58. package/docs/plugins/queue/rabbitmq.md +0 -34
  59. package/docs/plugins/queue/rabbitmq_amqplib.md +0 -51
  60. package/plugins/queue/rabbitmq.js +0 -141
  61. package/plugins/queue/rabbitmq_amqplib.js +0 -96
  62. package/test/config/tls/ec.pem +0 -23
  63. package/test/config/tls/mismatched.pem +0 -49
  64. package/test/outbound_bounce_net_errors.js +0 -157
  65. package/test/outbound_bounce_rfc3464.js +0 -366
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
- const assert = require('node:assert')
3
+ const assert = require('node:assert/strict')
4
+ const { describe, it, beforeEach } = require('node:test')
4
5
 
5
6
  const fixtures = require('haraka-test-fixtures')
6
7
  const outbound = require('../../outbound')
@@ -8,13 +9,12 @@ const TimerQueue = require('../../outbound/timer_queue')
8
9
 
9
10
  const Connection = fixtures.connection
10
11
 
11
- const _set_up = (done) => {
12
+ const _set_up = () => {
12
13
  this.plugin = new fixtures.plugin('status')
13
14
  this.plugin.outbound = outbound
14
15
 
15
16
  this.connection = Connection.createConnection()
16
17
  this.connection.remote.is_local = true
17
- done()
18
18
  }
19
19
 
20
20
  describe('status', () => {
@@ -29,7 +29,7 @@ describe('status', () => {
29
29
  describe('access', () => {
30
30
  beforeEach(_set_up)
31
31
 
32
- it('remote', (done) => {
32
+ it('remote', (t, done) => {
33
33
  this.connection.remote.is_local = false
34
34
  this.plugin.hook_unrecognized_command(
35
35
  (code) => {
@@ -45,7 +45,7 @@ describe('status', () => {
45
45
  describe('pools', () => {
46
46
  beforeEach(_set_up)
47
47
 
48
- it('list_pools', (done) => {
48
+ it('list_pools', (t, done) => {
49
49
  this.connection.respond = (code, message) => {
50
50
  const data = JSON.parse(message)
51
51
  assert.equal('object', typeof data) // there should be one pools array for noncluster and more for cluster
@@ -58,7 +58,7 @@ describe('status', () => {
58
58
  describe('queues', () => {
59
59
  beforeEach(_set_up)
60
60
 
61
- it('inspect_queue', (done) => {
61
+ it('inspect_queue', (t, done) => {
62
62
  // should list delivery_queue and temp_fail_queue per cluster children
63
63
  outbound.temp_fail_queue = new TimerQueue(10)
64
64
  outbound.temp_fail_queue.add('file1', 100, () => {})
@@ -73,7 +73,7 @@ describe('status', () => {
73
73
  this.plugin.hook_unrecognized_command(() => {}, this.connection, ['STATUS', 'QUEUE INSPECT'])
74
74
  })
75
75
 
76
- it('stat_queue', (done) => {
76
+ it('stat_queue', (t, done) => {
77
77
  // should list files only
78
78
  this.connection.respond = (code, message) => {
79
79
  const data = JSON.parse(message)
@@ -83,7 +83,7 @@ describe('status', () => {
83
83
  this.plugin.hook_unrecognized_command(() => {}, this.connection, ['STATUS', 'QUEUE STATS'])
84
84
  })
85
85
 
86
- it('list_queue', (done) => {
86
+ it('list_queue', (t, done) => {
87
87
  // should list files only
88
88
  this.connection.respond = (code, message) => {
89
89
  const data = JSON.parse(message)
@@ -93,7 +93,7 @@ describe('status', () => {
93
93
  this.plugin.hook_unrecognized_command(() => {}, this.connection, ['STATUS', 'QUEUE LIST'])
94
94
  })
95
95
 
96
- it('discard_from_queue', (done) => {
96
+ it('discard_from_queue', (t, done) => {
97
97
  const self = this
98
98
 
99
99
  outbound.temp_fail_queue = new TimerQueue(10)
@@ -118,7 +118,7 @@ describe('status', () => {
118
118
  )
119
119
  })
120
120
 
121
- it('push_email_at_queue', (done) => {
121
+ it('push_email_at_queue', (t, done) => {
122
122
  const timeout = setTimeout(() => {
123
123
  assert.ok(false, 'Timeout')
124
124
  done()
@@ -1,12 +1,13 @@
1
1
  'use strict'
2
2
 
3
- const assert = require('node:assert')
3
+ const assert = require('node:assert/strict')
4
4
  const path = require('node:path')
5
+ const { describe, it, beforeEach } = require('node:test')
5
6
 
6
7
  const fixtures = require('haraka-test-fixtures')
7
8
  const Plugin = fixtures.plugin
8
9
 
9
- const _set_up = (done) => {
10
+ const _set_up = () => {
10
11
  this.plugin = new Plugin('tls')
11
12
  this.connection = new fixtures.connection.createConnection()
12
13
 
@@ -15,33 +16,22 @@ const _set_up = (done) => {
15
16
  this.plugin.net_utils.config = this.plugin.net_utils.config.module_config(path.resolve('test'))
16
17
 
17
18
  this.plugin.tls_opts = {}
18
- done()
19
19
  }
20
20
 
21
21
  describe('tls', () => {
22
22
  beforeEach(_set_up)
23
23
 
24
- it('has function register', () => {
25
- assert.ok(this.plugin)
26
- assert.equal('function', typeof this.plugin.register)
27
- })
28
-
29
- it('has function upgrade_connection', () => {
30
- assert.equal('function', typeof this.plugin.upgrade_connection)
31
- })
32
-
33
- it('has function advertise_starttls', () => {
34
- assert.equal('function', typeof this.plugin.advertise_starttls)
35
- })
36
-
37
- it('has function emit_upgrade_msg', () => {
38
- assert.equal('function', typeof this.plugin.emit_upgrade_msg)
39
- })
24
+ const methods = ['register', 'upgrade_connection', 'advertise_starttls', 'emit_upgrade_msg']
25
+ for (const method of methods) {
26
+ it(`has function ${method}`, () => {
27
+ assert.equal(typeof this.plugin[method], 'function')
28
+ })
29
+ }
40
30
 
41
31
  describe('register', () => {
42
- it('with certs, should call register_hook()', () => {
32
+ it('with certs, should register hooks', () => {
43
33
  this.plugin.register()
44
- assert.ok(this.plugin.register_hook.called)
34
+ assert.ok(Object.keys(this.plugin.hooks).length)
45
35
  })
46
36
  })
47
37
 
@@ -0,0 +1,102 @@
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
+
8
+ const _set_up = () => {
9
+ this.plugin = new fixtures.plugin('xclient')
10
+ this.connection = fixtures.connection.createConnection()
11
+ this.connection.capabilities = []
12
+ }
13
+
14
+ describe('xclient', () => {
15
+ beforeEach(_set_up)
16
+
17
+ describe('hook_capabilities', () => {
18
+ const cases = [
19
+ { desc: 'adds XCLIENT for loopback IPv4 (127.0.0.1)', ip: '127.0.0.1', expected: true },
20
+ { desc: 'adds XCLIENT for loopback IPv6 (::1)', ip: '::1', expected: true },
21
+ { desc: 'does not add XCLIENT for non-loopback IP', ip: '10.0.0.1', expected: false },
22
+ ]
23
+
24
+ for (const { desc, ip, expected } of cases) {
25
+ it(desc, async () => {
26
+ this.connection.remote.ip = ip
27
+ await new Promise((resolve) => this.plugin.hook_capabilities(resolve, this.connection))
28
+ const hasXclient = this.connection.capabilities.some((c) => c.startsWith('XCLIENT'))
29
+ assert.equal(hasXclient, expected)
30
+ })
31
+ }
32
+ })
33
+
34
+ describe('hook_unrecognized_command', () => {
35
+ const callHook = (params) =>
36
+ new Promise((resolve) => {
37
+ this.plugin.hook_unrecognized_command((code) => resolve(code), this.connection, params)
38
+ })
39
+
40
+ const cases = [
41
+ {
42
+ desc: 'ignores non-XCLIENT commands',
43
+ params: ['EHLO', 'example.com'],
44
+ check: (code) => assert.equal(code, undefined),
45
+ },
46
+ {
47
+ desc: 'denies XCLIENT when transaction is in progress',
48
+ setup: () => this.connection.init_transaction(),
49
+ params: ['XCLIENT', 'ADDR=127.0.0.1'],
50
+ check: (code) => assert.equal(code, DENY),
51
+ },
52
+ {
53
+ desc: 'denies XCLIENT from disallowed IP',
54
+ setup: () => {
55
+ this.connection.remote.ip = '10.0.0.1'
56
+ },
57
+ params: ['XCLIENT', 'ADDR=127.0.0.2'],
58
+ check: (code) => assert.equal(code, DENY),
59
+ },
60
+ {
61
+ desc: 'denies XCLIENT with no valid IP address',
62
+ setup: () => {
63
+ this.connection.remote.ip = '127.0.0.1'
64
+ },
65
+ params: ['XCLIENT', 'NAME=example.com'],
66
+ check: (code) => assert.equal(code, DENY),
67
+ },
68
+ {
69
+ desc: 'accepts XCLIENT with valid IPv4 ADDR from allowed host',
70
+ setup: () => {
71
+ this.connection.remote.ip = '127.0.0.1'
72
+ },
73
+ params: ['XCLIENT', 'ADDR=1.2.3.4'],
74
+ check: (code) => assert.ok(code === NEXT_HOOK || code === undefined),
75
+ },
76
+ {
77
+ desc: 'accepts XCLIENT with valid IPv6 ADDR from allowed host',
78
+ setup: () => {
79
+ this.connection.remote.ip = '127.0.0.1'
80
+ },
81
+ params: ['XCLIENT', 'ADDR=IPV6:2001:db8::1'],
82
+ check: (code) => assert.ok(code === NEXT_HOOK || code === undefined),
83
+ },
84
+ {
85
+ desc: 'accepts XCLIENT with ADDR and NAME, skipping rdns lookup',
86
+ setup: () => {
87
+ this.connection.remote.ip = '127.0.0.1'
88
+ },
89
+ params: ['XCLIENT', 'ADDR=1.2.3.4 NAME=example.com'],
90
+ check: (code) => assert.equal(code, NEXT_HOOK),
91
+ },
92
+ ]
93
+
94
+ for (const { desc, setup, params, check } of cases) {
95
+ it(desc, async () => {
96
+ if (setup) setup()
97
+ const code = await callHook(params)
98
+ check(code)
99
+ })
100
+ }
101
+ })
102
+ })
package/test/plugins.js CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  process.env.WITHOUT_CONFIG_CACHE = true
4
4
 
5
- const assert = require('node:assert')
5
+ const { describe, it, beforeEach, afterEach } = require('node:test')
6
+ const assert = require('node:assert/strict')
6
7
  const fs = require('node:fs')
7
8
  const path = require('node:path')
8
9
 
@@ -49,7 +50,7 @@ describe('plugin', () => {
49
50
 
50
51
  describe('get_timeout', () => {
51
52
  const toPath = path.resolve('config', `${piName}.timeout`)
52
- it('0s', (done) => {
53
+ it('0s', (t, done) => {
53
54
  fs.writeFile(toPath, '0', () => {
54
55
  this.plugin = new plugin.Plugin(piName)
55
56
  assert.equal(this.plugin.timeout, 0)
@@ -57,7 +58,7 @@ describe('plugin', () => {
57
58
  })
58
59
  })
59
60
 
60
- it('3s', (done) => {
61
+ it('3s', (t, done) => {
61
62
  fs.writeFile(toPath, '3', () => {
62
63
  this.plugin = new plugin.Plugin(piName)
63
64
  assert.equal(this.plugin.timeout, 3)
@@ -65,7 +66,7 @@ describe('plugin', () => {
65
66
  })
66
67
  })
67
68
 
68
- it('60s', (done) => {
69
+ it('60s', (t, done) => {
69
70
  fs.writeFile(toPath, '60', () => {
70
71
  this.plugin = new plugin.Plugin(piName)
71
72
  assert.equal(this.plugin.timeout, 60)
@@ -73,7 +74,7 @@ describe('plugin', () => {
73
74
  })
74
75
  })
75
76
 
76
- it('30s default (overrides NaN)', (done) => {
77
+ it('30s default (overrides NaN)', (t, done) => {
77
78
  fs.writeFile(toPath, 'apple', () => {
78
79
  this.plugin = new plugin.Plugin(piName)
79
80
  assert.equal(this.plugin.timeout, 30)
@@ -83,14 +84,12 @@ describe('plugin', () => {
83
84
  })
84
85
 
85
86
  describe('plugin_paths', () => {
86
- beforeEach((done) => {
87
+ beforeEach(() => {
87
88
  delete process.env.HARAKA
88
- done()
89
89
  })
90
90
 
91
- afterEach((done) => {
91
+ afterEach(() => {
92
92
  delete process.env.HARAKA
93
- done()
94
93
  })
95
94
 
96
95
  it('CORE plugin: (tls)', () => {
@@ -190,14 +189,12 @@ describe('plugin', () => {
190
189
  })
191
190
 
192
191
  describe('plugin_config', () => {
193
- beforeEach((done) => {
192
+ beforeEach(() => {
194
193
  delete process.env.HARAKA
195
- done()
196
194
  })
197
195
 
198
- afterEach((done) => {
196
+ afterEach(() => {
199
197
  delete process.env.HARAKA
200
- done()
201
198
  })
202
199
 
203
200
  it('CORE plugin: (tls)', () => {
package/test/rfc1869.js CHANGED
@@ -1,66 +1,89 @@
1
- const assert = require('node:assert')
1
+ 'use strict'
2
+
3
+ const { describe, it } = require('node:test')
4
+ const assert = require('node:assert/strict')
2
5
 
3
6
  const { parse } = require('../rfc1869')
4
7
 
5
8
  function _check(line, expected) {
6
9
  const match = /^(MAIL|RCPT)\s+(.*)$/.exec(line)
7
10
  const parsed = parse(match[1].toLowerCase(), match[2])
8
- assert.equal(parsed.length, expected.length)
9
- for (let x = 0; x < expected.length; x++) {
10
- assert.equal(parsed[x], expected[x])
11
- }
11
+ assert.deepEqual(parsed, expected)
12
12
  }
13
13
 
14
14
  describe('rfc1869', () => {
15
- it('MAIL FROM:<>', () => {
16
- _check('MAIL FROM:<>', ['<>'])
17
- })
18
-
19
- it('MAIL FROM:', () => {
20
- _check('MAIL FROM:', ['<>'])
21
- })
22
-
23
- it('MAIL FROM:<postmaster>', () => {
24
- _check('MAIL FROM:<postmaster>', ['<postmaster>'])
25
- })
26
-
27
- it('MAIL FROM:user', () => {
28
- _check('MAIL FROM:user', ['user'])
29
- })
30
-
31
- it('MAIL FROM:user size=1234', () => {
32
- _check('MAIL FROM:user size=1234', ['user', 'size=1234'])
33
- })
15
+ describe('valid parse cases', () => {
16
+ const validCases = [
17
+ // MAIL FROM variants
18
+ ['MAIL FROM:<>', ['<>']],
19
+ ['MAIL FROM:', ['<>']],
20
+ ['MAIL FROM:<postmaster>', ['<postmaster>']],
21
+ ['MAIL FROM:user', ['user']],
22
+ ['MAIL FROM:user size=1234', ['user', 'size=1234']],
23
+ ['MAIL FROM:user@domain size=1234', ['user@domain', 'size=1234']],
24
+ ['MAIL FROM:<user@domain> size=1234', ['<user@domain>', 'size=1234']],
25
+ ['MAIL FROM:<user@domain> somekey', ['<user@domain>', 'somekey']],
26
+ ['MAIL FROM:<user@domain> somekey other=foo', ['<user@domain>', 'somekey', 'other=foo']],
27
+ // RFC 1652 BODY extension keyword
28
+ ['MAIL FROM:<user@domain> BODY=8BITMIME', ['<user@domain>', 'BODY=8BITMIME']],
29
+ // RFC 6531 SMTPUTF8 keyword (no value)
30
+ ['MAIL FROM:<user@domain> SMTPUTF8', ['<user@domain>', 'SMTPUTF8']],
31
+ // RCPT TO variants
32
+ ['RCPT TO: 0@mailblog.biz 0=9 1=9', ['<0@mailblog.biz>', '0=9', '1=9']],
33
+ ['RCPT TO:<r86x-ray@emailitin.com> state=1', ['<r86x-ray@emailitin.com>', 'state=1']],
34
+ ['RCPT TO:<user=name@domain.com> foo=bar', ['<user=name@domain.com>', 'foo=bar']],
35
+ ['RCPT TO:<postmaster>', ['<postmaster>']],
36
+ ['RCPT TO:<abuse>', ['<abuse>']],
37
+ ]
34
38
 
35
- it('MAIL FROM:user@domain size=1234', () => {
36
- _check('MAIL FROM:user@domain size=1234', ['user@domain', 'size=1234'])
39
+ for (const [line, expected] of validCases) {
40
+ it(line, () => _check(line, expected))
41
+ }
37
42
  })
38
43
 
39
- it('MAIL FROM:<user@domain> size=1234', () => {
40
- _check('MAIL FROM:<user@domain> size=1234', ['<user@domain>', 'size=1234'])
41
- })
44
+ describe('error cases', () => {
45
+ const throwCases = [
46
+ {
47
+ desc: 'MAIL FROM with space inside angle-bracket address',
48
+ args: ['mail', 'FROM:<user@dom ain>'],
49
+ },
50
+ {
51
+ desc: 'RCPT TO with syntax error in address (space in address)',
52
+ args: ['rcpt', 'TO: user @domain bad'],
53
+ },
54
+ {
55
+ desc: 'RCPT TO unknown address (no @ and not postmaster/abuse)',
56
+ args: ['rcpt', 'TO:unknown'],
57
+ },
58
+ ]
42
59
 
43
- it('MAIL FROM:<user@domain> somekey', () => {
44
- _check('MAIL FROM:<user@domain> somekey', ['<user@domain>', 'somekey'])
60
+ for (const { desc, args } of throwCases) {
61
+ it(`throws: ${desc}`, () => {
62
+ assert.throws(() => parse(...args), Error)
63
+ })
64
+ }
45
65
  })
46
66
 
47
- it('MAIL FROM:<user@domain> somekey other=foo', () => {
48
- _check('MAIL FROM:<user@domain> somekey other=foo', ['<user@domain>', 'somekey', 'other=foo'])
49
- })
50
-
51
- it('RCPT TO ugly', () => {
52
- _check('RCPT TO: 0@mailblog.biz 0=9 1=9', ['<0@mailblog.biz>', '0=9', '1=9'])
53
- })
54
-
55
- it('RCPT TO:<r86x-ray@emailitin.com> state=1', () => {
56
- _check('RCPT TO:<r86x-ray@emailitin.com> state=1', ['<r86x-ray@emailitin.com>', 'state=1'])
57
- })
58
-
59
- it('RCPT TO:<user=name@domain.com> foo=bar', () => {
60
- _check('RCPT TO:<user=name@domain.com> foo=bar', ['<user=name@domain.com>', 'foo=bar'])
61
- })
67
+ describe('strict mode', () => {
68
+ const strictValidCases = [
69
+ ['mail', 'FROM:<user@domain.com>', '<user@domain.com>'],
70
+ ['rcpt', 'TO:<user@domain.com>', '<user@domain.com>'],
71
+ ]
72
+ for (const [type, line, expected] of strictValidCases) {
73
+ it(`strict ${type.toUpperCase()} with angle brackets accepts address`, () => {
74
+ const result = parse(type, line, true)
75
+ assert.equal(result[0], expected)
76
+ })
77
+ }
62
78
 
63
- it('RCPT TO:<postmaster>', () => {
64
- _check('RCPT TO:<postmaster>', ['<postmaster>'])
79
+ const strictThrowCases = [
80
+ ['mail', 'FROM:user@domain.com'],
81
+ ['rcpt', 'TO:user@domain.com'],
82
+ ]
83
+ for (const [type, line] of strictThrowCases) {
84
+ it(`strict ${type.toUpperCase()} without angle brackets throws`, () => {
85
+ assert.throws(() => parse(type, line, true), Error)
86
+ })
87
+ }
65
88
  })
66
89
  })