Haraka 3.1.6 → 3.1.7

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 (53) hide show
  1. package/CHANGELOG.md +32 -1
  2. package/CONTRIBUTORS.md +8 -8
  3. package/Plugins.md +99 -99
  4. package/config/smtp_forward.ini +10 -0
  5. package/config/smtp_proxy.ini +10 -0
  6. package/connection.js +25 -8
  7. package/docs/plugins/queue/smtp_forward.md +19 -3
  8. package/docs/plugins/queue/smtp_proxy.md +10 -2
  9. package/haraka.js +1 -1
  10. package/outbound/hmail.js +39 -39
  11. package/outbound/index.js +4 -4
  12. package/outbound/tls.js +2 -43
  13. package/package.json +49 -48
  14. package/plugins/auth/auth_base.js +9 -3
  15. package/plugins/auth/auth_proxy.js +14 -11
  16. package/plugins/block_me.js +4 -2
  17. package/plugins/prevent_credential_leaks.js +3 -1
  18. package/plugins/process_title.js +6 -6
  19. package/plugins/queue/qmail-queue.js +15 -19
  20. package/plugins/queue/smtp_forward.js +12 -4
  21. package/plugins/queue/smtp_proxy.js +14 -3
  22. package/plugins/tls.js +13 -5
  23. package/plugins/xclient.js +3 -1
  24. package/server.js +5 -3
  25. package/smtp_client.js +20 -11
  26. package/test/config/block_me.recipient +1 -0
  27. package/test/config/block_me.senders +1 -0
  28. package/test/connection.js +24 -0
  29. package/test/outbound/bounce_net_errors.js +3 -2
  30. package/test/plugins/auth/auth_bridge.js +80 -0
  31. package/test/plugins/auth/flat_file.js +128 -0
  32. package/test/plugins/block_me.js +157 -0
  33. package/test/plugins/data.signatures.js +114 -0
  34. package/test/plugins/delay_deny.js +263 -0
  35. package/test/plugins/prevent_credential_leaks.js +178 -0
  36. package/test/plugins/process_title.js +135 -0
  37. package/test/plugins/queue/deliver.js +99 -0
  38. package/test/plugins/queue/discard.js +79 -0
  39. package/test/plugins/queue/lmtp.js +138 -0
  40. package/test/plugins/queue/qmail-queue.js +99 -0
  41. package/test/plugins/queue/quarantine.js +81 -0
  42. package/test/plugins/queue/smtp_bridge.js +154 -0
  43. package/test/plugins/queue/smtp_forward.js +42 -6
  44. package/test/plugins/queue/smtp_proxy.js +139 -0
  45. package/test/plugins/reseed_rng.js +34 -0
  46. package/test/plugins/tarpit.js +91 -0
  47. package/test/plugins/tls.js +25 -0
  48. package/test/plugins/toobusy.js +21 -0
  49. package/test/plugins/xclient.js +14 -0
  50. package/test/server.js +59 -0
  51. package/test/smtp_client.js +45 -12
  52. package/test/tls_socket.js +82 -0
  53. package/tls_socket.js +50 -0
@@ -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('address-rfc2821')
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
+ })
@@ -0,0 +1,263 @@
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
+ // params layout: [code, msg, pi_name, pi_function, pi_params, pi_hook]
10
+ function makeParams({ code = DENY, msg = 'test deny', name = 'some_plugin', fn = 'hook_fn', hook = 'ehlo' } = {}) {
11
+ return [code, msg, name, fn, null, hook]
12
+ }
13
+
14
+ describe('delay_deny', () => {
15
+ let plugin, conn
16
+
17
+ beforeEach(() => {
18
+ plugin = new fixtures.plugin('delay_deny')
19
+ plugin.config.get = () => ({ main: {} })
20
+ conn = fixtures.connection.createConnection()
21
+ conn.init_transaction()
22
+ })
23
+
24
+ describe('hook_deny', () => {
25
+ it('skips itself (pi_name === delay_deny)', (t, done) => {
26
+ plugin.hook_deny(
27
+ (rc) => {
28
+ assert.equal(rc, undefined)
29
+ done()
30
+ },
31
+ conn,
32
+ makeParams({ name: 'delay_deny' }),
33
+ )
34
+ })
35
+
36
+ it('stores connection-level pre-DATA deny for ehlo hook', (t, done) => {
37
+ const params = makeParams({ hook: 'ehlo' })
38
+ plugin.hook_deny(
39
+ (rc) => {
40
+ assert.equal(rc, OK)
41
+ assert.equal(conn.notes.delay_deny_pre.length, 1)
42
+ assert.equal(conn.notes.delay_deny_pre[0], params)
43
+ assert.equal(conn.notes.delay_deny_pre_fail['some_plugin'], 1)
44
+ done()
45
+ },
46
+ conn,
47
+ params,
48
+ )
49
+ })
50
+
51
+ it('stores connection-level pre-DATA deny for connect hook', (t, done) => {
52
+ const params = makeParams({ hook: 'connect' })
53
+ plugin.hook_deny(
54
+ (rc) => {
55
+ assert.equal(rc, OK)
56
+ assert.ok(conn.notes.delay_deny_pre.includes(params))
57
+ done()
58
+ },
59
+ conn,
60
+ params,
61
+ )
62
+ })
63
+
64
+ it('stores transaction-level pre-DATA deny for mail hook', (t, done) => {
65
+ const params = makeParams({ hook: 'mail' })
66
+ plugin.hook_deny(
67
+ (rc) => {
68
+ assert.equal(rc, OK)
69
+ assert.equal(conn.transaction.notes.delay_deny_pre.length, 1)
70
+ assert.equal(conn.transaction.notes.delay_deny_pre[0], params)
71
+ assert.equal(conn.transaction.notes.delay_deny_pre_fail['some_plugin'], 1)
72
+ done()
73
+ },
74
+ conn,
75
+ params,
76
+ )
77
+ })
78
+
79
+ it('stores transaction-level pre-DATA deny for rcpt hook', (t, done) => {
80
+ const params = makeParams({ hook: 'rcpt' })
81
+ plugin.hook_deny(
82
+ (rc) => {
83
+ assert.equal(rc, OK)
84
+ assert.equal(conn.transaction.notes.delay_deny_pre.length, 1)
85
+ assert.equal(conn.transaction.notes.delay_deny_pre[0], params)
86
+ done()
87
+ },
88
+ conn,
89
+ params,
90
+ )
91
+ })
92
+
93
+ it('calls next (no delay) for data_post hook', (t, done) => {
94
+ plugin.hook_deny(
95
+ (rc) => {
96
+ assert.equal(rc, undefined)
97
+ done()
98
+ },
99
+ conn,
100
+ makeParams({ hook: 'data_post' }),
101
+ )
102
+ })
103
+
104
+ it('calls next (no delay) for data hook', (t, done) => {
105
+ plugin.hook_deny(
106
+ (rc) => {
107
+ assert.equal(rc, undefined)
108
+ done()
109
+ },
110
+ conn,
111
+ makeParams({ hook: 'data' }),
112
+ )
113
+ })
114
+
115
+ it('delays when plugin is in included_plugins list', (t, done) => {
116
+ plugin.config.get = () => ({ main: { included_plugins: 'allowed_plugin' } })
117
+ const params = makeParams({ name: 'allowed_plugin', hook: 'ehlo' })
118
+ plugin.hook_deny(
119
+ (rc) => {
120
+ assert.equal(rc, OK)
121
+ done()
122
+ },
123
+ conn,
124
+ params,
125
+ )
126
+ })
127
+
128
+ it('passes through when plugin is not in included_plugins list', (t, done) => {
129
+ plugin.config.get = () => ({ main: { included_plugins: 'allowed_plugin' } })
130
+ plugin.hook_deny(
131
+ (rc) => {
132
+ assert.equal(rc, undefined)
133
+ done()
134
+ },
135
+ conn,
136
+ makeParams({ name: 'other_plugin', hook: 'ehlo' }),
137
+ )
138
+ })
139
+
140
+ it('passes through when plugin is in excluded_plugins list', (t, done) => {
141
+ plugin.config.get = () => ({ main: { excluded_plugins: 'skip_plugin' } })
142
+ plugin.hook_deny(
143
+ (rc) => {
144
+ assert.equal(rc, undefined)
145
+ done()
146
+ },
147
+ conn,
148
+ makeParams({ name: 'skip_plugin', hook: 'ehlo' }),
149
+ )
150
+ })
151
+
152
+ it('delays when plugin is not in excluded_plugins list', (t, done) => {
153
+ plugin.config.get = () => ({ main: { excluded_plugins: 'skip_plugin' } })
154
+ const params = makeParams({ name: 'other_plugin', hook: 'ehlo' })
155
+ plugin.hook_deny(
156
+ (rc) => {
157
+ assert.equal(rc, OK)
158
+ done()
159
+ },
160
+ conn,
161
+ params,
162
+ )
163
+ })
164
+
165
+ it('can exclude by plugin:hook format', (t, done) => {
166
+ plugin.config.get = () => ({ main: { excluded_plugins: 'some_plugin:helo' } })
167
+ plugin.hook_deny(
168
+ (rc) => {
169
+ assert.equal(rc, undefined)
170
+ done()
171
+ },
172
+ conn,
173
+ makeParams({ name: 'some_plugin', hook: 'helo' }),
174
+ )
175
+ })
176
+
177
+ it('can exclude by plugin:hook:function format', (t, done) => {
178
+ plugin.config.get = () => ({ main: { excluded_plugins: 'some_plugin:ehlo:hook_fn' } })
179
+ plugin.hook_deny(
180
+ (rc) => {
181
+ assert.equal(rc, undefined)
182
+ done()
183
+ },
184
+ conn,
185
+ makeParams({ name: 'some_plugin', fn: 'hook_fn', hook: 'ehlo' }),
186
+ )
187
+ })
188
+ })
189
+
190
+ describe('hook_rcpt_ok', () => {
191
+ it('calls next when there is no transaction', (t, done) => {
192
+ conn.transaction = null
193
+ plugin.hook_rcpt_ok((rc) => {
194
+ assert.equal(rc, undefined)
195
+ done()
196
+ }, conn)
197
+ })
198
+
199
+ it('bypasses all denies when connection is relaying', (t, done) => {
200
+ conn.relaying = true
201
+ conn.notes.delay_deny_pre = [makeParams({ hook: 'ehlo' })]
202
+ plugin.hook_rcpt_ok((rc) => {
203
+ assert.equal(rc, undefined)
204
+ done()
205
+ }, conn)
206
+ })
207
+
208
+ it('applies deferred connection-level deny', (t, done) => {
209
+ conn.relaying = false
210
+ conn.notes.delay_deny_pre = [[DENY, 'deferred ehlo deny', 'check_relay', 'fn', null, 'ehlo']]
211
+ plugin.hook_rcpt_ok((rc, msg) => {
212
+ assert.equal(rc, DENY)
213
+ assert.equal(msg, 'deferred ehlo deny')
214
+ done()
215
+ }, conn)
216
+ })
217
+
218
+ it('applies deferred transaction-level deny', (t, done) => {
219
+ conn.relaying = false
220
+ conn.transaction.notes.delay_deny_pre = [[DENYSOFT, 'deferred mail deny', 'check_helo', 'fn', null, 'mail']]
221
+ plugin.hook_rcpt_ok((rc, msg) => {
222
+ assert.equal(rc, DENYSOFT)
223
+ assert.equal(msg, 'deferred mail deny')
224
+ done()
225
+ }, conn)
226
+ })
227
+
228
+ it('calls next when no deferred denies are present', (t, done) => {
229
+ conn.relaying = false
230
+ plugin.hook_rcpt_ok((rc) => {
231
+ assert.equal(rc, undefined)
232
+ done()
233
+ }, conn)
234
+ })
235
+ })
236
+
237
+ describe('hook_data', () => {
238
+ it('calls next when no pre-DATA failures exist', (t, done) => {
239
+ plugin.hook_data((rc) => {
240
+ assert.equal(rc, undefined)
241
+ done()
242
+ }, conn)
243
+ })
244
+
245
+ it('calls next when transaction is missing', (t, done) => {
246
+ conn.transaction = null
247
+ plugin.hook_data((rc) => {
248
+ assert.equal(rc, undefined)
249
+ done()
250
+ }, conn)
251
+ })
252
+
253
+ it('calls next with no action when transaction has pre-DATA failures', (t, done) => {
254
+ // Note: fails.push.apply(Object.keys(...)) in the plugin is a pre-existing bug —
255
+ // the array receives no items so the header is never added.
256
+ conn.transaction.notes.delay_deny_pre_fail = { bad_plugin: 1, another_plugin: 1 }
257
+ plugin.hook_data((rc) => {
258
+ assert.equal(rc, undefined)
259
+ done()
260
+ }, conn)
261
+ })
262
+ })
263
+ })