Haraka 3.1.4 → 3.1.6

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 (55) hide show
  1. package/.prettierignore +1 -1
  2. package/{Changes.md → CHANGELOG.md} +34 -0
  3. package/CONTRIBUTORS.md +26 -26
  4. package/README.md +68 -93
  5. package/SECURITY.md +178 -0
  6. package/bin/haraka +7 -14
  7. package/config/plugins +0 -3
  8. package/docs/Connection.md +126 -39
  9. package/docs/CoreConfig.md +92 -74
  10. package/docs/HAProxy.md +41 -25
  11. package/docs/Logging.md +68 -38
  12. package/docs/Outbound.md +124 -179
  13. package/docs/Plugins.md +38 -59
  14. package/docs/Transaction.md +78 -83
  15. package/docs/Tutorial.md +122 -209
  16. package/docs/plugins/aliases.md +1 -141
  17. package/docs/plugins/auth/auth_ldap.md +2 -39
  18. package/docs/plugins/max_unrecognized_commands.md +4 -18
  19. package/docs/plugins/process_title.md +3 -3
  20. package/docs/plugins/reseed_rng.md +11 -13
  21. package/docs/plugins/tls.md +7 -7
  22. package/docs/plugins/toobusy.md +10 -4
  23. package/docs/tutorials/SettingUpOutbound.md +40 -48
  24. package/endpoint.js +32 -2
  25. package/outbound/hmail.js +3 -2
  26. package/outbound/index.js +3 -0
  27. package/package.json +21 -34
  28. package/plugins/queue/smtp_forward.js +4 -4
  29. package/run_tests +3 -15
  30. package/server.js +17 -7
  31. package/smtp_client.js +8 -6
  32. package/test/connection.js +234 -0
  33. package/test/endpoint.js +32 -4
  34. package/test/host_pool.js +57 -31
  35. package/test/logger.js +75 -135
  36. package/test/outbound/bounce_net_errors.js +87 -131
  37. package/test/outbound/bounce_rfc3464.js +177 -254
  38. package/test/outbound/hmail.js +19 -0
  39. package/test/outbound/index.js +189 -0
  40. package/test/outbound/queue.js +92 -0
  41. package/test/plugins/auth/auth_base.js +39 -44
  42. package/test/plugins/auth/auth_vpopmaild.js +8 -9
  43. package/test/plugins/queue/smtp_forward.js +953 -183
  44. package/test/plugins/rcpt_to.host_list_base.js +58 -93
  45. package/test/plugins/rcpt_to.in_host_list.js +126 -175
  46. package/test/plugins/record_envelope_addresses.js +8 -8
  47. package/test/plugins/status.js +10 -10
  48. package/test/plugins/tls.js +9 -19
  49. package/test/plugins/xclient.js +75 -110
  50. package/test/plugins.js +10 -13
  51. package/test/rfc1869.js +50 -70
  52. package/test/server.js +438 -421
  53. package/test/smtp_client.js +1192 -218
  54. package/test/tls_socket.js +242 -0
  55. package/tls_socket.js +18 -22
package/test/logger.js CHANGED
@@ -1,9 +1,11 @@
1
- const assert = require('node:assert')
1
+ 'use strict'
2
+
3
+ const { describe, it, beforeEach } = require('node:test')
4
+ const assert = require('node:assert/strict')
2
5
  const util = require('node:util')
3
6
 
4
- const _set_up = (done) => {
7
+ const _set_up = () => {
5
8
  this.logger = require('../logger')
6
- done()
7
9
  }
8
10
 
9
11
  describe('logger', () => {
@@ -16,55 +18,23 @@ describe('logger', () => {
16
18
  })
17
19
 
18
20
  describe('log', () => {
19
- it('log', () => {
20
- this.logger.deferred_logs = []
21
- assert.equal(0, this.logger.deferred_logs.length)
22
- assert.ok(this.logger.log('WARN', 'test warning'))
23
- assert.equal(1, this.logger.deferred_logs.length)
24
- })
25
-
26
- it('log, w/deferred', () => {
27
- this.logger.plugins = { plugin_list: true }
28
- this.logger.deferred_logs.push({
29
- level: 'INFO',
30
- data: 'log test info',
21
+ const formats = ['DEFAULT', 'LOGFMT', 'JSON']
22
+
23
+ for (const fmt of formats) {
24
+ it(`log in ${fmt} format`, () => {
25
+ this.logger.deferred_logs = []
26
+ this.logger.format = this.logger.formats[fmt]
27
+ assert.ok(this.logger.log('WARN', 'test warning'))
28
+ assert.equal(this.logger.deferred_logs.length, 1)
31
29
  })
32
- assert.ok(this.logger.log('INFO', 'another test info'))
33
- })
34
30
 
35
- it('log in logfmt', () => {
36
- this.logger.deferred_logs = []
37
- this.logger.format = this.logger.formats.LOGFMT
38
- assert.equal(0, this.logger.deferred_logs.length)
39
- assert.ok(this.logger.log('WARN', 'test warning'))
40
- assert.equal(1, this.logger.deferred_logs.length)
41
- })
42
-
43
- it('log in logfmt w/deferred', () => {
44
- this.logger.plugins = { plugin_list: true }
45
- this.logger.deferred_logs.push({
46
- level: 'INFO',
47
- data: 'log test info',
31
+ it(`log in ${fmt} format w/deferred`, () => {
32
+ this.logger.format = this.logger.formats[fmt]
33
+ this.logger.plugins = { plugin_list: true }
34
+ this.logger.deferred_logs.push({ level: 'INFO', data: 'log test info' })
35
+ assert.ok(this.logger.log('INFO', 'another test info'))
48
36
  })
49
- assert.ok(this.logger.log('INFO', 'another test info'))
50
- })
51
-
52
- it('log in json', () => {
53
- this.logger.deferred_logs = []
54
- this.logger.format = this.logger.formats.JSON
55
- assert.equal(0, this.logger.deferred_logs.length)
56
- assert.ok(this.logger.log('WARN', 'test warning'))
57
- assert.equal(1, this.logger.deferred_logs.length)
58
- })
59
-
60
- it('log in json w/deferred', () => {
61
- this.logger.plugins = { plugin_list: true }
62
- this.logger.deferred_logs.push({
63
- level: 'INFO',
64
- data: 'log test info',
65
- })
66
- assert.ok(this.logger.log('INFO', 'another test info'))
67
- })
37
+ }
68
38
  })
69
39
 
70
40
  describe('level', () => {
@@ -75,68 +45,40 @@ describe('logger', () => {
75
45
  })
76
46
 
77
47
  describe('set_format', () => {
78
- it('set format to DEFAULT', () => {
79
- this.logger.format = ''
80
- this.logger.set_format('DEFAULT')
81
- assert.equal(this.logger.format, this.logger.formats.DEFAULT)
82
- })
83
-
84
- it('set format to LOGFMT', () => {
85
- this.logger.format = ''
86
- this.logger.set_format('LOGFMT')
87
- assert.equal(this.logger.format, this.logger.formats.LOGFMT)
88
- })
89
-
90
- it('set format to JSON', () => {
91
- this.logger.format = ''
92
- this.logger.set_format('JSON')
93
- assert.equal(this.logger.format, this.logger.formats.JSON)
94
- })
95
-
96
- it('set format to DEFAULT if empty', () => {
97
- this.logger.format = ''
98
- this.logger.set_format('')
99
- assert.equal(this.logger.format, this.logger.formats.DEFAULT)
100
- })
101
-
102
- it('set format to DEFAULT if lowercase', () => {
103
- this.logger.format = ''
104
- this.logger.set_format('default')
105
- assert.equal(this.logger.format, this.logger.formats.DEFAULT)
106
- })
107
-
108
- it('set format to DEFAULT if invalid', () => {
109
- this.logger.format = ''
110
- this.logger.set_format('invalid')
111
- assert.equal(this.logger.format, this.logger.formats.DEFAULT)
112
- })
48
+ // [input, expected format key]
49
+ const cases = [
50
+ ['DEFAULT', 'DEFAULT'],
51
+ ['LOGFMT', 'LOGFMT'],
52
+ ['JSON', 'JSON'],
53
+ ['', 'DEFAULT'], // empty → DEFAULT
54
+ ['default', 'DEFAULT'], // case-insensitive → DEFAULT
55
+ ['invalid', 'DEFAULT'], // unknown → DEFAULT
56
+ ]
57
+ for (const [input, expectedKey] of cases) {
58
+ it(`set_format(${JSON.stringify(input)}) → ${expectedKey}`, () => {
59
+ this.logger.format = ''
60
+ this.logger.set_format(input)
61
+ assert.equal(this.logger.format, this.logger.formats[expectedKey])
62
+ })
63
+ }
113
64
  })
114
65
 
115
66
  describe('set_loglevel', () => {
116
- it('set loglevel to LOGINFO', () => {
117
- this.logger.set_loglevel('LOGINFO')
118
- assert.equal(this.logger.loglevel, this.logger.levels.LOGINFO)
119
- })
120
-
121
- it('set loglevel to INFO', () => {
122
- this.logger.set_loglevel('INFO')
123
- assert.equal(this.logger.loglevel, this.logger.levels.INFO)
124
- })
125
-
126
- it('set loglevel to EMERG', () => {
127
- this.logger.set_loglevel('emerg')
128
- assert.equal(this.logger.loglevel, this.logger.levels.EMERG)
129
- })
130
-
131
- it('set loglevel to 6', () => {
132
- this.logger.set_loglevel(6)
133
- assert.equal(this.logger.loglevel, 6)
134
- })
135
-
136
- it('set loglevel to WARN if invalid', () => {
137
- this.logger.set_loglevel('invalid')
138
- assert.equal(this.logger.loglevel, this.logger.levels.WARN)
139
- })
67
+ // [input, expected level key or null for numeric assertion]
68
+ const cases = [
69
+ ['LOGINFO', 'LOGINFO'],
70
+ ['INFO', 'INFO'],
71
+ ['emerg', 'EMERG'], // case-insensitive
72
+ [6, null], // numeric passthrough
73
+ ['invalid', 'WARN'], // unknown → WARN
74
+ ]
75
+ for (const [input, expectedKey] of cases) {
76
+ it(`set_loglevel(${JSON.stringify(input)}) → ${expectedKey ?? input}`, () => {
77
+ this.logger.set_loglevel(input)
78
+ const expected = expectedKey ? this.logger.levels[expectedKey] : input
79
+ assert.equal(this.logger.loglevel, expected)
80
+ })
81
+ }
140
82
  })
141
83
 
142
84
  describe('set_timestamps', () => {
@@ -219,40 +161,38 @@ describe('logger', () => {
219
161
  })
220
162
 
221
163
  describe('log_if_level', () => {
222
- it('log_if_level is a function', () => {
223
- assert.ok('function' === typeof this.logger.log_if_level)
164
+ it('is a function', () => {
165
+ assert.equal(typeof this.logger.log_if_level, 'function')
224
166
  })
225
167
 
226
- it('log_if_level test log entry', () => {
168
+ it('returns a logging function', () => {
227
169
  this.logger.loglevel = 9
228
170
  const f = this.logger.log_if_level('INFO', 'LOGINFO')
229
- assert.ok(f)
230
- assert.ok('function' === typeof f)
231
- assert.ok(f('test info message'))
232
- assert.equal(1, this.logger.deferred_logs.length)
233
- // console.log(this.logger.deferred_logs[0]);
234
- assert.equal('INFO', this.logger.deferred_logs[0].level)
235
- })
236
-
237
- it('log_if_level null case', () => {
238
- this.logger.loglevel = 9
239
- const f = this.logger.log_if_level('INFO', 'LOGINFO')
240
- assert.ok(f(null))
241
- assert.equal(2, this.logger.deferred_logs.length)
242
- })
243
-
244
- it('log_if_level false', () => {
245
- this.logger.loglevel = 9
246
- const f = this.logger.log_if_level('INFO', 'LOGINFO')
247
- assert.ok(f(false))
248
- assert.equal(3, this.logger.deferred_logs.length)
249
- })
171
+ assert.equal(typeof f, 'function')
172
+ })
173
+
174
+ // Each of these runs independently with a fresh deferred_logs
175
+ for (const [label, msg] of [
176
+ ['string', 'test info message'],
177
+ ['null', null],
178
+ ['false', false],
179
+ ['0 (falsy number)', 0],
180
+ ]) {
181
+ it(`logs ${label} value and appends to deferred_logs`, () => {
182
+ this.logger.loglevel = 9
183
+ this.logger.deferred_logs = []
184
+ const f = this.logger.log_if_level('INFO', 'LOGINFO')
185
+ assert.ok(f(msg))
186
+ assert.equal(this.logger.deferred_logs.length, 1)
187
+ })
188
+ }
250
189
 
251
- it('log_if_level 0', () => {
190
+ it('records correct level in deferred log entry', () => {
252
191
  this.logger.loglevel = 9
192
+ this.logger.deferred_logs = []
253
193
  const f = this.logger.log_if_level('INFO', 'LOGINFO')
254
- assert.ok(f(0))
255
- assert.equal(4, this.logger.deferred_logs.length)
194
+ f('test info message')
195
+ assert.equal(this.logger.deferred_logs[0].level, 'INFO')
256
196
  })
257
197
  })
258
198
 
@@ -26,151 +26,107 @@ const queue_dir = path.resolve(__dirname, '../test-queue')
26
26
 
27
27
  // ── Helpers ───────────────────────────────────────────────────────────────────
28
28
 
29
- const ensureQueueDir = () =>
29
+ const ensureQueueDir = () => fs.promises.mkdir(queue_dir, { recursive: true })
30
+
31
+ const cleanQueueDir = async () => {
32
+ if (!fs.existsSync(queue_dir)) return
33
+ for (const file of fs.readdirSync(queue_dir)) {
34
+ const full = path.resolve(queue_dir, file)
35
+ if (fs.lstatSync(full).isDirectory()) throw new Error(`unexpected subdirectory: ${full}`)
36
+ fs.unlinkSync(full)
37
+ }
38
+ }
39
+
40
+ /** Creates a mock HMailItem, resolving with it or rejecting on error. */
41
+ const mockHMailItem = (ctx, opts = {}) =>
30
42
  new Promise((resolve, reject) => {
31
- fs.exists(queue_dir, (exists) => {
32
- if (exists) return resolve()
33
- fs.mkdir(queue_dir, (err) => (err ? reject(err) : resolve()))
34
- })
43
+ util_hmailitem.newMockHMailItem(ctx, reject, opts, resolve)
35
44
  })
36
45
 
37
- const cleanQueueDir = () =>
46
+ /**
47
+ * Intercepts `HMailItem.prototype[method]`, calls `assertion(this)` when invoked,
48
+ * then triggers the action under test via `trigger()`.
49
+ */
50
+ const interceptAndAssert = (method, assertion, trigger) =>
38
51
  new Promise((resolve, reject) => {
39
- fs.exists(queue_dir, (exists) => {
40
- if (!exists) return resolve()
52
+ const orig = HMailItem.prototype[method]
53
+ HMailItem.prototype[method] = function () {
41
54
  try {
42
- for (const file of fs.readdirSync(queue_dir)) {
43
- const full = path.resolve(queue_dir, file)
44
- if (fs.lstatSync(full).isDirectory()) return reject(new Error(`unexpected subdirectory: ${full}`))
45
- fs.unlinkSync(full)
46
- }
55
+ assertion(this)
47
56
  resolve()
48
- } catch (err) {
49
- reject(err)
57
+ } catch (e) {
58
+ reject(e)
59
+ } finally {
60
+ HMailItem.prototype[method] = orig
50
61
  }
51
- })
52
- })
53
-
54
- /** Creates a mock HMailItem, resolving with it or rejecting on error. */
55
- const mockHMailItem = (ctx, opts = {}) =>
56
- new Promise((resolve, reject) => {
57
- util_hmailitem.newMockHMailItem(ctx, reject, opts, resolve)
62
+ }
63
+ trigger()
58
64
  })
59
65
 
60
66
  // ── Tests ─────────────────────────────────────────────────────────────────────
61
67
 
68
+ // [method, dsn_status, optional setup fn, trigger fn, test name]
69
+ const testCases = [
70
+ {
71
+ name: 'get_mx=DENY triggers bounce with dsn_status 5.1.2',
72
+ method: 'bounce',
73
+ status: '5.1.2',
74
+ setup: (h) => {
75
+ h.domain = h.todo.domain
76
+ },
77
+ trigger: (h) => HMailItem.prototype.get_mx_respond.apply(h, [constants.deny, {}]),
78
+ },
79
+ {
80
+ name: 'get_mx=DENYSOFT triggers temp_fail with dsn_status 4.1.2',
81
+ method: 'temp_fail',
82
+ status: '4.1.2',
83
+ setup: (h) => {
84
+ h.domain = h.todo.domain
85
+ },
86
+ trigger: (h) => HMailItem.prototype.get_mx_respond.apply(h, [constants.denysoft, {}]),
87
+ },
88
+ {
89
+ name: 'get_mx_error({code:NXDOMAIN}) triggers bounce with dsn_status 5.1.2',
90
+ method: 'bounce',
91
+ status: '5.1.2',
92
+ trigger: (h) => HMailItem.prototype.get_mx_error.apply(h, [{ code: dns.NXDOMAIN }]),
93
+ },
94
+ {
95
+ name: "get_mx_error({code:'SOME-OTHER-ERR'}) triggers temp_fail with dsn_status 4.1.0",
96
+ method: 'temp_fail',
97
+ status: '4.1.0',
98
+ trigger: (h) => HMailItem.prototype.get_mx_error.apply(h, [{ code: 'SOME-OTHER-ERR' }, {}]),
99
+ },
100
+ {
101
+ name: 'found_mx with empty exchange triggers bounce with dsn_status 5.1.2',
102
+ method: 'bounce',
103
+ status: '5.1.2',
104
+ trigger: (h) => HMailItem.prototype.found_mx.apply(h, [[{ priority: 0, exchange: '' }]]),
105
+ },
106
+ {
107
+ name: 'try_deliver with empty mxlist triggers temp_fail with dsn_status 5.1.2',
108
+ method: 'temp_fail',
109
+ status: '5.1.2',
110
+ setup: (h) => {
111
+ h.mxlist = []
112
+ },
113
+ trigger: (h) => HMailItem.prototype.try_deliver.apply(h, []),
114
+ },
115
+ ]
116
+
62
117
  describe('outbound_bounce_net_errors', () => {
63
118
  beforeEach(ensureQueueDir)
64
119
  afterEach(cleanQueueDir)
65
120
 
66
- it('get_mx=DENY triggers bounce with dsn_status 5.1.2', async () => {
67
- const mock_hmail = await mockHMailItem(outbound_context)
68
- await new Promise((resolve, reject) => {
69
- const orig = HMailItem.prototype.bounce
70
- HMailItem.prototype.bounce = function (err, opts) {
71
- try {
72
- assert.equal(this.todo.rcpt_to[0].dsn_status, '5.1.2', 'dsn status')
73
- resolve()
74
- } catch (e) {
75
- reject(e)
76
- } finally {
77
- HMailItem.prototype.bounce = orig
78
- }
79
- }
80
- mock_hmail.domain = mock_hmail.todo.domain
81
- HMailItem.prototype.get_mx_respond.apply(mock_hmail, [constants.deny, {}])
82
- })
83
- })
84
-
85
- it('get_mx=DENYSOFT triggers temp_fail with dsn_status 4.1.2', async () => {
86
- const mock_hmail = await mockHMailItem(outbound_context)
87
- await new Promise((resolve, reject) => {
88
- const orig = HMailItem.prototype.temp_fail
89
- HMailItem.prototype.temp_fail = function (err, opts) {
90
- try {
91
- assert.equal(this.todo.rcpt_to[0].dsn_status, '4.1.2', 'dsn status')
92
- resolve()
93
- } catch (e) {
94
- reject(e)
95
- } finally {
96
- HMailItem.prototype.temp_fail = orig
97
- }
98
- }
99
- mock_hmail.domain = mock_hmail.todo.domain
100
- HMailItem.prototype.get_mx_respond.apply(mock_hmail, [constants.denysoft, {}])
101
- })
102
- })
103
-
104
- it('get_mx_error({code:NXDOMAIN}) triggers bounce with dsn_status 5.1.2', async () => {
105
- const mock_hmail = await mockHMailItem(outbound_context)
106
- await new Promise((resolve, reject) => {
107
- const orig = HMailItem.prototype.bounce
108
- HMailItem.prototype.bounce = function (err, opts) {
109
- try {
110
- assert.equal(this.todo.rcpt_to[0].dsn_status, '5.1.2', 'dsn status')
111
- resolve()
112
- } catch (e) {
113
- reject(e)
114
- } finally {
115
- HMailItem.prototype.bounce = orig
116
- }
117
- }
118
- HMailItem.prototype.get_mx_error.apply(mock_hmail, [{ code: dns.NXDOMAIN }])
119
- })
120
- })
121
-
122
- it("get_mx_error({code:'SOME-OTHER-ERR'}) triggers temp_fail with dsn_status 4.1.0", async () => {
123
- const mock_hmail = await mockHMailItem(outbound_context)
124
- await new Promise((resolve, reject) => {
125
- const orig = HMailItem.prototype.temp_fail
126
- HMailItem.prototype.temp_fail = function (err, opts) {
127
- try {
128
- assert.equal(this.todo.rcpt_to[0].dsn_status, '4.1.0', 'dsn status')
129
- resolve()
130
- } catch (e) {
131
- reject(e)
132
- } finally {
133
- HMailItem.prototype.temp_fail = orig
134
- }
135
- }
136
- HMailItem.prototype.get_mx_error.apply(mock_hmail, [{ code: 'SOME-OTHER-ERR' }, {}])
121
+ for (const { name, method, status, setup, trigger } of testCases) {
122
+ it(name, async () => {
123
+ const mock_hmail = await mockHMailItem(outbound_context)
124
+ if (setup) setup(mock_hmail)
125
+ await interceptAndAssert(
126
+ method,
127
+ (h) => assert.equal(h.todo.rcpt_to[0].dsn_status, status, 'dsn_status'),
128
+ () => trigger(mock_hmail),
129
+ )
137
130
  })
138
- })
139
-
140
- it("found_mx(null, [{priority:0,exchange:''}]) triggers bounce with dsn_status 5.1.2", async () => {
141
- const mock_hmail = await mockHMailItem(outbound_context)
142
- await new Promise((resolve, reject) => {
143
- const orig = HMailItem.prototype.bounce
144
- HMailItem.prototype.bounce = function (err, opts) {
145
- try {
146
- assert.equal(this.todo.rcpt_to[0].dsn_status, '5.1.2', 'dsn status')
147
- resolve()
148
- } catch (e) {
149
- reject(e)
150
- } finally {
151
- HMailItem.prototype.bounce = orig
152
- }
153
- }
154
- HMailItem.prototype.found_mx.apply(mock_hmail, [[{ priority: 0, exchange: '' }]])
155
- })
156
- })
157
-
158
- it('try_deliver with empty mxlist triggers temp_fail with dsn_status 5.1.2', async () => {
159
- const mock_hmail = await mockHMailItem(outbound_context)
160
- mock_hmail.mxlist = []
161
- await new Promise((resolve, reject) => {
162
- const orig = HMailItem.prototype.temp_fail
163
- HMailItem.prototype.temp_fail = function (err, opts) {
164
- try {
165
- assert.equal(this.todo.rcpt_to[0].dsn_status, '5.1.2', 'dsn status')
166
- resolve()
167
- } catch (e) {
168
- reject(e)
169
- } finally {
170
- HMailItem.prototype.temp_fail = orig
171
- }
172
- }
173
- HMailItem.prototype.try_deliver.apply(mock_hmail, [])
174
- })
175
- })
131
+ }
176
132
  })