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
|
@@ -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
|
+
})
|
|
@@ -0,0 +1,178 @@
|
|
|
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({ authUser, authPasswd, bodytext = '', children = [] } = {}) {
|
|
10
|
+
const conn = fixtures.connection.createConnection()
|
|
11
|
+
conn.init_transaction()
|
|
12
|
+
if (authUser) conn.notes.auth_user = authUser
|
|
13
|
+
if (authPasswd) conn.notes.auth_passwd = authPasswd
|
|
14
|
+
conn.transaction.body = { bodytext, children }
|
|
15
|
+
return conn
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('prevent_credential_leaks', () => {
|
|
19
|
+
let plugin
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
plugin = new fixtures.plugin('prevent_credential_leaks')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('hook_data', () => {
|
|
26
|
+
it('does not enable parse_body when no auth credentials', (t, done) => {
|
|
27
|
+
const conn = makeConnection()
|
|
28
|
+
conn.transaction.parse_body = false
|
|
29
|
+
plugin.hook_data((rc) => {
|
|
30
|
+
assert.equal(rc, undefined)
|
|
31
|
+
assert.equal(conn.transaction.parse_body, false)
|
|
32
|
+
done()
|
|
33
|
+
}, conn)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('enables parse_body when both auth_user and auth_passwd are present', (t, done) => {
|
|
37
|
+
const conn = makeConnection({ authUser: 'user@example.com', authPasswd: 'secret' })
|
|
38
|
+
conn.transaction.parse_body = false
|
|
39
|
+
plugin.hook_data((rc) => {
|
|
40
|
+
assert.equal(rc, undefined)
|
|
41
|
+
assert.equal(conn.transaction.parse_body, true)
|
|
42
|
+
done()
|
|
43
|
+
}, conn)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('does not enable parse_body when only auth_user is set', (t, done) => {
|
|
47
|
+
const conn = makeConnection({ authUser: 'user@example.com' })
|
|
48
|
+
conn.transaction.parse_body = false
|
|
49
|
+
plugin.hook_data((rc) => {
|
|
50
|
+
assert.equal(rc, undefined)
|
|
51
|
+
assert.equal(conn.transaction.parse_body, false)
|
|
52
|
+
done()
|
|
53
|
+
}, conn)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('handles missing connection gracefully', (t, done) => {
|
|
57
|
+
// Simulate a null-ish connection by calling with empty notes
|
|
58
|
+
const conn = fixtures.connection.createConnection()
|
|
59
|
+
conn.init_transaction()
|
|
60
|
+
conn.notes = {}
|
|
61
|
+
plugin.hook_data((rc) => {
|
|
62
|
+
assert.equal(rc, undefined)
|
|
63
|
+
done()
|
|
64
|
+
}, conn)
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('hook_data_post', () => {
|
|
69
|
+
it('calls next when no auth credentials are set', (t, done) => {
|
|
70
|
+
const conn = makeConnection({ bodytext: 'user@example.com secret123' })
|
|
71
|
+
plugin.hook_data_post((rc) => {
|
|
72
|
+
assert.equal(rc, undefined)
|
|
73
|
+
done()
|
|
74
|
+
}, conn)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('calls next when only auth_user is set (no password)', (t, done) => {
|
|
78
|
+
const conn = makeConnection({ authUser: 'user@example.com', bodytext: 'user@example.com' })
|
|
79
|
+
plugin.hook_data_post((rc) => {
|
|
80
|
+
assert.equal(rc, undefined)
|
|
81
|
+
done()
|
|
82
|
+
}, conn)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('calls next when body contains neither username nor password', (t, done) => {
|
|
86
|
+
const conn = makeConnection({
|
|
87
|
+
authUser: 'alice@example.com',
|
|
88
|
+
authPasswd: 'mypassword',
|
|
89
|
+
bodytext: 'Hello, this is a clean email with no credentials.',
|
|
90
|
+
})
|
|
91
|
+
plugin.hook_data_post((rc) => {
|
|
92
|
+
assert.equal(rc, undefined)
|
|
93
|
+
done()
|
|
94
|
+
}, conn)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('calls next when body contains username but not password', (t, done) => {
|
|
98
|
+
const conn = makeConnection({
|
|
99
|
+
authUser: 'alice@example.com',
|
|
100
|
+
authPasswd: 'mypassword',
|
|
101
|
+
bodytext: 'Contact alice@example.com for more info.',
|
|
102
|
+
})
|
|
103
|
+
plugin.hook_data_post((rc) => {
|
|
104
|
+
assert.equal(rc, undefined)
|
|
105
|
+
done()
|
|
106
|
+
}, conn)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('denies when body contains both username and password', (t, done) => {
|
|
110
|
+
const conn = makeConnection({
|
|
111
|
+
authUser: 'alice@example.com',
|
|
112
|
+
authPasswd: 'mypassword',
|
|
113
|
+
bodytext: 'Please send your login: alice and password: mypassword to activate.',
|
|
114
|
+
})
|
|
115
|
+
plugin.hook_data_post((rc, msg) => {
|
|
116
|
+
assert.equal(rc, DENY)
|
|
117
|
+
assert.ok(msg.includes('Credential leak'))
|
|
118
|
+
done()
|
|
119
|
+
}, conn)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('denies when credentials appear in a child body part', (t, done) => {
|
|
123
|
+
const conn = fixtures.connection.createConnection()
|
|
124
|
+
conn.init_transaction()
|
|
125
|
+
conn.notes.auth_user = 'bob@example.com'
|
|
126
|
+
conn.notes.auth_passwd = 's3cr3t'
|
|
127
|
+
conn.transaction.body = {
|
|
128
|
+
bodytext: 'clean parent text',
|
|
129
|
+
children: [{ bodytext: 'bob login with s3cr3t password', children: [] }],
|
|
130
|
+
}
|
|
131
|
+
plugin.hook_data_post((rc) => {
|
|
132
|
+
assert.equal(rc, DENY)
|
|
133
|
+
done()
|
|
134
|
+
}, conn)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('handles qualified username (user@domain) by making domain optional', (t, done) => {
|
|
138
|
+
const conn = makeConnection({
|
|
139
|
+
authUser: 'carol@corp.example.com',
|
|
140
|
+
authPasswd: 'pass123',
|
|
141
|
+
bodytext: 'carol pass123 credentials',
|
|
142
|
+
})
|
|
143
|
+
plugin.hook_data_post((rc) => {
|
|
144
|
+
assert.equal(rc, DENY)
|
|
145
|
+
done()
|
|
146
|
+
}, conn)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('unqualified username (no @) is not split into a partial match', (t, done) => {
|
|
150
|
+
// Bug: `if (idx)` with idx === -1 treated 'admin' as qualified,
|
|
151
|
+
// splitting it to user='admi' which then matches 'admiral'.
|
|
152
|
+
const conn = makeConnection({
|
|
153
|
+
authUser: 'admin',
|
|
154
|
+
authPasswd: 'pw',
|
|
155
|
+
bodytext: 'the admiral said pw today',
|
|
156
|
+
})
|
|
157
|
+
plugin.hook_data_post((rc) => {
|
|
158
|
+
assert.equal(rc, undefined)
|
|
159
|
+
done()
|
|
160
|
+
}, conn)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('calls next when credentials appear in neither top nor child', (t, done) => {
|
|
164
|
+
const conn = fixtures.connection.createConnection()
|
|
165
|
+
conn.init_transaction()
|
|
166
|
+
conn.notes.auth_user = 'dave@example.com'
|
|
167
|
+
conn.notes.auth_passwd = 'xyzzy'
|
|
168
|
+
conn.transaction.body = {
|
|
169
|
+
bodytext: 'Hello world',
|
|
170
|
+
children: [{ bodytext: 'No credentials here at all', children: [] }],
|
|
171
|
+
}
|
|
172
|
+
plugin.hook_data_post((rc) => {
|
|
173
|
+
assert.equal(rc, undefined)
|
|
174
|
+
done()
|
|
175
|
+
}, conn)
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
})
|
|
@@ -0,0 +1,135 @@
|
|
|
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
|
+
const Notes = require('haraka-notes')
|
|
8
|
+
|
|
9
|
+
function makeServer(extra = {}) {
|
|
10
|
+
return {
|
|
11
|
+
notes: new Notes({
|
|
12
|
+
pt_connections: 0,
|
|
13
|
+
pt_concurrent: 0,
|
|
14
|
+
pt_cps_diff: 0,
|
|
15
|
+
pt_cps_max: 0,
|
|
16
|
+
pt_recipients: 0,
|
|
17
|
+
pt_rps_diff: 0,
|
|
18
|
+
pt_rps_max: 0,
|
|
19
|
+
pt_messages: 0,
|
|
20
|
+
pt_mps_diff: 0,
|
|
21
|
+
pt_mps_max: 0,
|
|
22
|
+
...extra,
|
|
23
|
+
}),
|
|
24
|
+
cluster: null,
|
|
25
|
+
address: () => ({ address: '127.0.0.1', port: 25 }),
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('process_title', () => {
|
|
30
|
+
let plugin
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
plugin = new fixtures.plugin('process_title')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('hook_connect_init', () => {
|
|
37
|
+
it('increments connection and concurrent counts', (t, done) => {
|
|
38
|
+
const server = makeServer()
|
|
39
|
+
const conn = fixtures.connection.createConnection({}, server)
|
|
40
|
+
plugin.hook_connect_init((rc) => {
|
|
41
|
+
assert.equal(rc, undefined)
|
|
42
|
+
assert.equal(server.notes.pt_connections, 1)
|
|
43
|
+
assert.equal(server.notes.pt_concurrent, 1)
|
|
44
|
+
assert.equal(conn.notes.pt_connect_run, true)
|
|
45
|
+
done()
|
|
46
|
+
}, conn)
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('hook_disconnect', () => {
|
|
51
|
+
it('decrements concurrent count when connect_init ran', (t, done) => {
|
|
52
|
+
const server = makeServer({ pt_connections: 1, pt_concurrent: 1 })
|
|
53
|
+
const conn = fixtures.connection.createConnection({}, server)
|
|
54
|
+
conn.notes.pt_connect_run = true
|
|
55
|
+
plugin.hook_disconnect((rc) => {
|
|
56
|
+
assert.equal(rc, undefined)
|
|
57
|
+
assert.equal(server.notes.pt_concurrent, 0)
|
|
58
|
+
assert.equal(server.notes.pt_connections, 1) // not re-incremented
|
|
59
|
+
done()
|
|
60
|
+
}, conn)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('increments connection count when connect_init did not run', (t, done) => {
|
|
64
|
+
const server = makeServer({ pt_connections: 0, pt_concurrent: 0 })
|
|
65
|
+
const conn = fixtures.connection.createConnection({}, server)
|
|
66
|
+
// pt_connect_run is NOT set: disconnect does connect bookkeeping then decrements
|
|
67
|
+
plugin.hook_disconnect((rc) => {
|
|
68
|
+
assert.equal(rc, undefined)
|
|
69
|
+
assert.equal(server.notes.pt_connections, 1) // incremented by disconnect
|
|
70
|
+
assert.equal(server.notes.pt_concurrent, 0) // +1 then -1
|
|
71
|
+
done()
|
|
72
|
+
}, conn)
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe('hook_rcpt', () => {
|
|
77
|
+
it('increments recipient count', (t, done) => {
|
|
78
|
+
const server = makeServer()
|
|
79
|
+
const conn = fixtures.connection.createConnection({}, server)
|
|
80
|
+
plugin.hook_rcpt((rc) => {
|
|
81
|
+
assert.equal(rc, undefined)
|
|
82
|
+
assert.equal(server.notes.pt_recipients, 1)
|
|
83
|
+
done()
|
|
84
|
+
}, conn)
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe('hook_data', () => {
|
|
89
|
+
it('increments message count', (t, done) => {
|
|
90
|
+
const server = makeServer()
|
|
91
|
+
const conn = fixtures.connection.createConnection({}, server)
|
|
92
|
+
plugin.hook_data((rc) => {
|
|
93
|
+
assert.equal(rc, undefined)
|
|
94
|
+
assert.equal(server.notes.pt_messages, 1)
|
|
95
|
+
done()
|
|
96
|
+
}, conn)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe('hook_init_child', () => {
|
|
101
|
+
it('initializes server notes and calls next', (t, done) => {
|
|
102
|
+
const server = { notes: new Notes(), cluster: null }
|
|
103
|
+
plugin.hook_init_child((rc) => {
|
|
104
|
+
clearInterval(plugin._interval)
|
|
105
|
+
assert.equal(rc, undefined)
|
|
106
|
+
assert.equal(server.notes.pt_connections, 0)
|
|
107
|
+
assert.equal(server.notes.pt_messages, 0)
|
|
108
|
+
assert.equal(server.notes.pt_recipients, 0)
|
|
109
|
+
done()
|
|
110
|
+
}, server)
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
describe('hook_init_master', () => {
|
|
115
|
+
it('initializes server notes and calls next (no cluster)', (t, done) => {
|
|
116
|
+
const server = { notes: new Notes(), cluster: null }
|
|
117
|
+
plugin.hook_init_master((rc) => {
|
|
118
|
+
clearInterval(plugin._interval)
|
|
119
|
+
assert.equal(rc, undefined)
|
|
120
|
+
assert.equal(server.notes.pt_connections, 0)
|
|
121
|
+
assert.equal(server.notes.pt_child_exits, 0)
|
|
122
|
+
done()
|
|
123
|
+
}, server)
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe('shutdown', () => {
|
|
128
|
+
it('clears the interval', () => {
|
|
129
|
+
plugin._interval = setInterval(() => {}, 9999)
|
|
130
|
+
plugin.shutdown()
|
|
131
|
+
// If the interval was cleared, no error thrown
|
|
132
|
+
assert.ok(true)
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
})
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert')
|
|
4
|
+
const path = require('node:path')
|
|
5
|
+
const Module = require('node:module')
|
|
6
|
+
const { describe, it, beforeEach, before, after } = require('node:test')
|
|
7
|
+
|
|
8
|
+
const fixtures = require('haraka-test-fixtures')
|
|
9
|
+
|
|
10
|
+
// deliver.js does `require('./outbound')` at the top level. In a running
|
|
11
|
+
// Haraka that resolves to the core outbound module (Haraka/outbound/index.js),
|
|
12
|
+
// which we don't want to load here: it would pull in the real delivery
|
|
13
|
+
// machinery. So we intercept Module._resolveFilename to map './outbound' to a
|
|
14
|
+
// stable path and pre-populate the require cache with a mock at that path
|
|
15
|
+
// before loading the plugin.
|
|
16
|
+
const outboundPath = path.resolve('outbound/index.js')
|
|
17
|
+
let mockOutbound
|
|
18
|
+
let origResolve
|
|
19
|
+
|
|
20
|
+
before(() => {
|
|
21
|
+
require('haraka-constants').import(global)
|
|
22
|
+
|
|
23
|
+
mockOutbound = { send_trans_email: () => {} }
|
|
24
|
+
require.cache[outboundPath] = {
|
|
25
|
+
id: outboundPath,
|
|
26
|
+
filename: outboundPath,
|
|
27
|
+
loaded: true,
|
|
28
|
+
exports: mockOutbound,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
origResolve = Module._resolveFilename
|
|
32
|
+
Module._resolveFilename = function (request, parent, isMain, options) {
|
|
33
|
+
if (request === './outbound') return outboundPath
|
|
34
|
+
return origResolve.call(this, request, parent, isMain, options)
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
after(() => {
|
|
39
|
+
Module._resolveFilename = origResolve
|
|
40
|
+
delete require.cache[outboundPath]
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
function makeConnection(opts = {}) {
|
|
44
|
+
const conn = fixtures.connection.createConnection()
|
|
45
|
+
conn.init_transaction()
|
|
46
|
+
if (opts.relaying !== undefined) conn.relaying = opts.relaying
|
|
47
|
+
return conn
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('queue/deliver', () => {
|
|
51
|
+
describe('hook_queue_outbound', () => {
|
|
52
|
+
let plugin, conn
|
|
53
|
+
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
plugin = new fixtures.plugin('queue/deliver')
|
|
56
|
+
mockOutbound.send_trans_email = () => {}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('calls next() when connection is not relaying', (t, done) => {
|
|
60
|
+
conn = makeConnection({ relaying: false })
|
|
61
|
+
plugin.hook_queue_outbound((rc) => {
|
|
62
|
+
assert.equal(rc, undefined)
|
|
63
|
+
done()
|
|
64
|
+
}, conn)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('calls next() when connection is undefined', (t, done) => {
|
|
68
|
+
plugin.hook_queue_outbound((rc) => {
|
|
69
|
+
assert.equal(rc, undefined)
|
|
70
|
+
done()
|
|
71
|
+
}, undefined)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('calls outbound.send_trans_email when relaying is true', (t, done) => {
|
|
75
|
+
conn = makeConnection({ relaying: true })
|
|
76
|
+
mockOutbound.send_trans_email = (txn, next) => {
|
|
77
|
+
assert.equal(txn, conn.transaction)
|
|
78
|
+
next(OK)
|
|
79
|
+
}
|
|
80
|
+
plugin.hook_queue_outbound((rc) => {
|
|
81
|
+
assert.equal(rc, OK)
|
|
82
|
+
done()
|
|
83
|
+
}, conn)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('passes transaction to outbound.send_trans_email', (t, done) => {
|
|
87
|
+
conn = makeConnection({ relaying: true })
|
|
88
|
+
let capturedTxn
|
|
89
|
+
mockOutbound.send_trans_email = (txn, next) => {
|
|
90
|
+
capturedTxn = txn
|
|
91
|
+
next(OK)
|
|
92
|
+
}
|
|
93
|
+
plugin.hook_queue_outbound(() => {
|
|
94
|
+
assert.equal(capturedTxn, conn.transaction)
|
|
95
|
+
done()
|
|
96
|
+
}, conn)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
})
|