Haraka 3.1.4 → 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.
- package/CONTRIBUTORS.md +1 -1
- package/Changes.md +14 -0
- package/package.json +8 -10
- package/plugins/queue/smtp_forward.js +4 -4
- package/run_tests +3 -15
- package/smtp_client.js +8 -6
- package/test/endpoint.js +5 -4
- package/test/host_pool.js +57 -31
- package/test/logger.js +75 -135
- package/test/outbound/bounce_net_errors.js +87 -131
- package/test/outbound/bounce_rfc3464.js +177 -254
- package/test/plugins/auth/auth_base.js +39 -44
- package/test/plugins/auth/auth_vpopmaild.js +8 -9
- package/test/plugins/queue/smtp_forward.js +953 -183
- package/test/plugins/rcpt_to.host_list_base.js +58 -93
- package/test/plugins/rcpt_to.in_host_list.js +126 -175
- package/test/plugins/record_envelope_addresses.js +8 -8
- package/test/plugins/status.js +10 -10
- package/test/plugins/tls.js +9 -19
- package/test/plugins/xclient.js +75 -110
- package/test/plugins.js +10 -13
- package/test/rfc1869.js +50 -70
- package/test/server.js +281 -436
- package/test/smtp_client.js +1192 -218
- package/test/tls_socket.js +104 -0
- package/tls_socket.js +16 -20
|
@@ -1,228 +1,998 @@
|
|
|
1
1
|
'use strict'
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
const { describe, it, beforeEach, afterEach } = require('node:test')
|
|
4
|
+
const assert = require('node:assert/strict')
|
|
5
|
+
const { EventEmitter } = require('node:events')
|
|
3
6
|
const path = require('node:path')
|
|
4
7
|
|
|
5
8
|
const { Address } = require('address-rfc2821')
|
|
6
9
|
const fixtures = require('haraka-test-fixtures')
|
|
7
10
|
const Notes = require('haraka-notes')
|
|
8
11
|
|
|
12
|
+
// Haraka result codes (haraka-constants)
|
|
9
13
|
const OK = 906
|
|
14
|
+
const DENY = 902
|
|
15
|
+
const DENYSOFT = 903
|
|
16
|
+
|
|
17
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
function makePlugin() {
|
|
20
|
+
const p = new fixtures.plugin('queue/smtp_forward')
|
|
21
|
+
p.config = p.config.module_config(path.resolve('test'))
|
|
22
|
+
p.register()
|
|
23
|
+
// Deep-clone cfg to prevent shared haraka-config reference mutations across tests
|
|
24
|
+
p.cfg = JSON.parse(JSON.stringify(p.cfg))
|
|
25
|
+
return p
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function makeConnection() {
|
|
29
|
+
const conn = fixtures.connection.createConnection()
|
|
30
|
+
conn.init_transaction()
|
|
31
|
+
conn.server = { notes: {} }
|
|
32
|
+
return conn
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function makeHmail(notes = {}) {
|
|
36
|
+
const n = new Notes()
|
|
37
|
+
for (const [k, v] of Object.entries(notes)) n.set(k, v)
|
|
38
|
+
return { todo: { notes: n } }
|
|
39
|
+
}
|
|
10
40
|
|
|
11
|
-
|
|
12
|
-
|
|
41
|
+
/** Mock SMTPClient returned by get_client_plugin stubs. */
|
|
42
|
+
class MockSMTPClient extends EventEmitter {
|
|
43
|
+
constructor() {
|
|
44
|
+
super()
|
|
45
|
+
this.smtp_utf8 = false
|
|
46
|
+
this.response = ['250 OK']
|
|
47
|
+
this.next = null
|
|
48
|
+
this.commands = []
|
|
49
|
+
}
|
|
13
50
|
|
|
14
|
-
|
|
15
|
-
|
|
51
|
+
call_next(code, msg) {
|
|
52
|
+
if (this.next) {
|
|
53
|
+
const n = this.next
|
|
54
|
+
delete this.next
|
|
55
|
+
n(code, msg)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
16
58
|
|
|
17
|
-
|
|
18
|
-
|
|
59
|
+
release() {
|
|
60
|
+
this.released = true
|
|
61
|
+
}
|
|
19
62
|
|
|
20
|
-
|
|
21
|
-
|
|
63
|
+
is_dead_sender() {
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
22
66
|
|
|
23
|
-
|
|
67
|
+
send_command(cmd, data) {
|
|
68
|
+
this.commands.push(data !== undefined ? `${cmd} ${data}` : cmd)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
start_data(stream) {
|
|
72
|
+
this.started = true
|
|
73
|
+
}
|
|
24
74
|
}
|
|
25
75
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
it('TLS enabled but no outbound config in tls.ini', () => {
|
|
29
|
-
const plugin = new fixtures.plugin('queue/smtp_forward')
|
|
30
|
-
plugin.register()
|
|
76
|
+
// Temporarily replace smtp_client_mod.get_client_plugin for queue_forward tests
|
|
77
|
+
const smtp_client_mod = require('../../../smtp_client')
|
|
31
78
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
79
|
+
function stubGetClientPlugin(factory) {
|
|
80
|
+
const orig = smtp_client_mod.get_client_plugin
|
|
81
|
+
smtp_client_mod.get_client_plugin = factory
|
|
82
|
+
return () => {
|
|
83
|
+
smtp_client_mod.get_client_plugin = orig
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ─── register ────────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
describe('smtp_forward register', () => {
|
|
90
|
+
it('registers the queue hook', () => {
|
|
91
|
+
const plugin = makePlugin()
|
|
92
|
+
assert.ok(plugin.hooks.queue)
|
|
35
93
|
})
|
|
36
94
|
|
|
37
|
-
|
|
38
|
-
|
|
95
|
+
it('registers the get_mx hook', () => {
|
|
96
|
+
const plugin = makePlugin()
|
|
97
|
+
assert.ok(plugin.hooks.get_mx)
|
|
98
|
+
})
|
|
39
99
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
100
|
+
it('registers check_sender hook when check_sender=true', () => {
|
|
101
|
+
const plugin = new fixtures.plugin('queue/smtp_forward')
|
|
102
|
+
plugin.config = plugin.config.module_config(path.resolve('test'))
|
|
103
|
+
plugin.load_smtp_forward_ini = function () {
|
|
104
|
+
this.cfg = {
|
|
105
|
+
main: { check_sender: true, check_recipient: true, enable_outbound: true, host: 'localhost', port: 25 },
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
plugin.register()
|
|
109
|
+
assert.ok(plugin.hooks.mail)
|
|
44
110
|
})
|
|
45
111
|
|
|
46
|
-
|
|
47
|
-
|
|
112
|
+
it('registers check_recipient hook when check_recipient=true', () => {
|
|
113
|
+
const plugin = new fixtures.plugin('queue/smtp_forward')
|
|
114
|
+
plugin.config = plugin.config.module_config(path.resolve('test'))
|
|
115
|
+
plugin.load_smtp_forward_ini = function () {
|
|
116
|
+
this.cfg = { main: { check_recipient: true, host: 'localhost', port: 25 } }
|
|
117
|
+
}
|
|
118
|
+
plugin.register()
|
|
119
|
+
assert.ok(plugin.hooks.rcpt)
|
|
120
|
+
})
|
|
48
121
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
122
|
+
it('registers queue_outbound hook when enable_outbound=true', () => {
|
|
123
|
+
const plugin = new fixtures.plugin('queue/smtp_forward')
|
|
124
|
+
plugin.config = plugin.config.module_config(path.resolve('test'))
|
|
125
|
+
plugin.load_smtp_forward_ini = function () {
|
|
126
|
+
this.cfg = { main: { enable_outbound: true, host: 'localhost', port: 25 } }
|
|
127
|
+
}
|
|
128
|
+
plugin.register()
|
|
129
|
+
assert.ok(plugin.hooks.queue_outbound)
|
|
130
|
+
})
|
|
55
131
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
132
|
+
it('aborts registration when load_errs is non-empty', () => {
|
|
133
|
+
const plugin = new fixtures.plugin('queue/smtp_forward')
|
|
134
|
+
plugin.config = plugin.config.module_config(path.resolve('test'))
|
|
135
|
+
plugin.load_smtp_forward_ini = function () {
|
|
136
|
+
this.cfg = { main: {} }
|
|
137
|
+
this.load_errs.push('simulated error')
|
|
138
|
+
}
|
|
139
|
+
plugin.register()
|
|
140
|
+
assert.equal(plugin.hooks.queue, undefined)
|
|
141
|
+
})
|
|
63
142
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
143
|
+
it('TLS enabled but no outbound config in tls.ini', () => {
|
|
144
|
+
const plugin = new fixtures.plugin('queue/smtp_forward')
|
|
145
|
+
plugin.register()
|
|
146
|
+
assert.equal(plugin.tls_options, undefined)
|
|
147
|
+
assert.ok(Object.keys(plugin.hooks).length)
|
|
148
|
+
})
|
|
149
|
+
})
|
|
71
150
|
|
|
72
|
-
|
|
73
|
-
this.connection.transaction.rcpt_to.push(new Address('<matt@test.com>'))
|
|
74
|
-
assert.deepEqual(this.plugin.get_config(this.connection), {
|
|
75
|
-
host: '1.2.3.4',
|
|
76
|
-
enable_tls: true,
|
|
77
|
-
auth_user: 'postmaster@test.com',
|
|
78
|
-
auth_pass: 'superDuperSecret',
|
|
79
|
-
})
|
|
80
|
-
})
|
|
151
|
+
// ─── load_smtp_forward_ini ────────────────────────────────────────────────────
|
|
81
152
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
})
|
|
89
|
-
})
|
|
153
|
+
describe('smtp_forward load_smtp_forward_ini', () => {
|
|
154
|
+
it('loads configuration from ini file', () => {
|
|
155
|
+
const plugin = makePlugin()
|
|
156
|
+
assert.ok(plugin.cfg.main)
|
|
157
|
+
assert.equal(plugin.cfg.main.host, 'localhost')
|
|
158
|
+
})
|
|
90
159
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
160
|
+
it('sets up a reload callback', () => {
|
|
161
|
+
// Calling load_smtp_forward_ini again should not crash
|
|
162
|
+
const plugin = makePlugin()
|
|
163
|
+
assert.doesNotThrow(() => plugin.load_smtp_forward_ini())
|
|
164
|
+
assert.ok(plugin.cfg.main)
|
|
165
|
+
})
|
|
166
|
+
})
|
|
96
167
|
|
|
97
|
-
|
|
98
|
-
this.plugin.cfg.main.domain_selector = 'mail_from'
|
|
99
|
-
this.connection.transaction.mail_from = new Address('<>')
|
|
100
|
-
const cfg = this.plugin.get_config(this.connection)
|
|
101
|
-
assert.equal(cfg.host, 'localhost')
|
|
102
|
-
assert.equal(cfg.enable_tls, true)
|
|
103
|
-
assert.equal(cfg.one_message_per_rcpt, true)
|
|
104
|
-
})
|
|
168
|
+
// ─── get_config ───────────────────────────────────────────────────────────────
|
|
105
169
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
})
|
|
170
|
+
describe('smtp_forward get_config', () => {
|
|
171
|
+
let plugin, connection
|
|
172
|
+
|
|
173
|
+
beforeEach(() => {
|
|
174
|
+
plugin = makePlugin()
|
|
175
|
+
connection = makeConnection()
|
|
113
176
|
})
|
|
114
177
|
|
|
115
|
-
|
|
116
|
-
|
|
178
|
+
it('returns main cfg when no transaction', () => {
|
|
179
|
+
connection.transaction = null
|
|
180
|
+
const cfg = plugin.get_config(connection)
|
|
181
|
+
assert.equal(cfg.host, 'localhost')
|
|
182
|
+
})
|
|
117
183
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
done()
|
|
124
|
-
},
|
|
125
|
-
this.hmail,
|
|
126
|
-
'undefined.com',
|
|
127
|
-
)
|
|
128
|
-
})
|
|
184
|
+
it('returns main cfg when no rcpt_to (no domain_selector set)', () => {
|
|
185
|
+
const cfg = plugin.get_config(connection)
|
|
186
|
+
assert.equal(cfg.host, 'localhost')
|
|
187
|
+
assert.equal(cfg.enable_tls, true)
|
|
188
|
+
})
|
|
129
189
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
assert.equal(code, undefined)
|
|
136
|
-
assert.deepEqual(mx, undefined)
|
|
137
|
-
done()
|
|
138
|
-
},
|
|
139
|
-
this.hmail,
|
|
140
|
-
'undefined.com',
|
|
141
|
-
)
|
|
142
|
-
})
|
|
190
|
+
it('returns main cfg for null recipient', () => {
|
|
191
|
+
connection.transaction.rcpt_to.push(new Address('<>'))
|
|
192
|
+
const cfg = plugin.get_config(connection)
|
|
193
|
+
assert.equal(cfg.host, 'localhost')
|
|
194
|
+
})
|
|
143
195
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
priority: 0,
|
|
150
|
-
exchange: '1.2.3.4',
|
|
151
|
-
port: 2555,
|
|
152
|
-
auth_user: 'postmaster@test.com',
|
|
153
|
-
auth_pass: 'superDuperSecret',
|
|
154
|
-
})
|
|
155
|
-
done()
|
|
156
|
-
},
|
|
157
|
-
this.hmail,
|
|
158
|
-
'test.com',
|
|
159
|
-
)
|
|
160
|
-
})
|
|
196
|
+
it('returns main cfg for unknown recipient domain', () => {
|
|
197
|
+
connection.transaction.rcpt_to.push(new Address('<matt@example.com>'))
|
|
198
|
+
const cfg = plugin.get_config(connection)
|
|
199
|
+
assert.equal(cfg.host, 'localhost')
|
|
200
|
+
})
|
|
161
201
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
assert.deepEqual(mx, {
|
|
169
|
-
priority: 0,
|
|
170
|
-
port: 465,
|
|
171
|
-
exchange: '4.3.2.1',
|
|
172
|
-
})
|
|
173
|
-
done()
|
|
174
|
-
},
|
|
175
|
-
this.hmail,
|
|
176
|
-
'undefined.com',
|
|
177
|
-
)
|
|
178
|
-
})
|
|
202
|
+
it('returns domain config for known recipient domain', () => {
|
|
203
|
+
connection.transaction.rcpt_to.push(new Address('<matt@test.com>'))
|
|
204
|
+
const cfg = plugin.get_config(connection)
|
|
205
|
+
assert.equal(cfg.host, '1.2.3.4')
|
|
206
|
+
assert.equal(cfg.auth_user, 'postmaster@test.com')
|
|
207
|
+
})
|
|
179
208
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
(code, mx) => {
|
|
185
|
-
assert.equal(code, OK)
|
|
186
|
-
assert.deepEqual(mx, {
|
|
187
|
-
priority: 0,
|
|
188
|
-
port: 24,
|
|
189
|
-
using_lmtp: true,
|
|
190
|
-
exchange: '4.3.2.1',
|
|
191
|
-
})
|
|
192
|
-
done()
|
|
193
|
-
},
|
|
194
|
-
this.hmail,
|
|
195
|
-
'undefined.com',
|
|
196
|
-
)
|
|
197
|
-
})
|
|
209
|
+
it('returns domain config with different TLS setting', () => {
|
|
210
|
+
connection.transaction.rcpt_to.push(new Address('<matt@test1.com>'))
|
|
211
|
+
const cfg = plugin.get_config(connection)
|
|
212
|
+
assert.deepEqual(cfg, { host: '1.2.3.4', enable_tls: false })
|
|
198
213
|
})
|
|
199
214
|
|
|
200
|
-
|
|
201
|
-
|
|
215
|
+
it('returns main cfg when domain_selector=mail_from but mail_from is null', () => {
|
|
216
|
+
plugin.cfg.main.domain_selector = 'mail_from'
|
|
217
|
+
connection.transaction.mail_from = null
|
|
218
|
+
const cfg = plugin.get_config(connection)
|
|
219
|
+
assert.equal(cfg.host, 'localhost')
|
|
220
|
+
})
|
|
202
221
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
222
|
+
it('returns main cfg when domain_selector=mail_from and null sender', () => {
|
|
223
|
+
plugin.cfg.main.domain_selector = 'mail_from'
|
|
224
|
+
connection.transaction.mail_from = new Address('<>')
|
|
225
|
+
const cfg = plugin.get_config(connection)
|
|
226
|
+
assert.equal(cfg.host, 'localhost')
|
|
227
|
+
})
|
|
206
228
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
229
|
+
it('returns domain config for mail_from domain_selector', () => {
|
|
230
|
+
plugin.cfg.main.domain_selector = 'mail_from'
|
|
231
|
+
connection.transaction.mail_from = new Address('<matt@test2.com>')
|
|
232
|
+
const cfg = plugin.get_config(connection)
|
|
233
|
+
assert.equal(cfg.host, '2.3.4.5')
|
|
234
|
+
})
|
|
212
235
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
236
|
+
it('returns config by full email address when present', () => {
|
|
237
|
+
plugin.cfg.main.domain_selector = 'mail_from'
|
|
238
|
+
plugin.cfg['specific@test.com'] = { host: 'specific.example.com' }
|
|
239
|
+
connection.transaction.mail_from = new Address('<specific@test.com>')
|
|
240
|
+
const cfg = plugin.get_config(connection)
|
|
241
|
+
assert.equal(cfg.host, 'specific.example.com')
|
|
242
|
+
})
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
// ─── check_sender ────────────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
describe('smtp_forward check_sender', () => {
|
|
248
|
+
let plugin, connection
|
|
249
|
+
|
|
250
|
+
beforeEach(() => {
|
|
251
|
+
plugin = makePlugin()
|
|
252
|
+
connection = makeConnection()
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it('returns without calling next when no transaction', () => {
|
|
256
|
+
connection.transaction = null
|
|
257
|
+
let nextCalled = false
|
|
258
|
+
plugin.check_sender(
|
|
259
|
+
() => {
|
|
260
|
+
nextCalled = true
|
|
261
|
+
},
|
|
262
|
+
connection,
|
|
263
|
+
[new Address('<a@test.com>')],
|
|
264
|
+
)
|
|
265
|
+
assert.equal(nextCalled, false)
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('skips and calls next() for null/empty sender', () => {
|
|
269
|
+
let code
|
|
270
|
+
plugin.check_sender(
|
|
271
|
+
(c) => {
|
|
272
|
+
code = c
|
|
273
|
+
},
|
|
274
|
+
connection,
|
|
275
|
+
[new Address('<>')],
|
|
276
|
+
)
|
|
277
|
+
assert.equal(code, undefined) // next() with no args
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('calls next() when sender domain not in config', () => {
|
|
281
|
+
let called = false
|
|
282
|
+
plugin.check_sender(
|
|
283
|
+
() => {
|
|
284
|
+
called = true
|
|
285
|
+
},
|
|
286
|
+
connection,
|
|
287
|
+
[new Address('<user@unknown.com>')],
|
|
288
|
+
)
|
|
289
|
+
assert.ok(called)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('denies spoofed MAIL FROM (domain in cfg, not relaying)', () => {
|
|
293
|
+
connection.relaying = false
|
|
294
|
+
let code
|
|
295
|
+
plugin.check_sender(
|
|
296
|
+
(c) => {
|
|
297
|
+
code = c
|
|
298
|
+
},
|
|
299
|
+
connection,
|
|
300
|
+
[new Address('<user@test.com>')],
|
|
301
|
+
)
|
|
302
|
+
assert.equal(code, DENY)
|
|
303
|
+
const r = connection.transaction.results.get(plugin)
|
|
304
|
+
assert.ok(r.fail.includes('mail_from!spoof'))
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
it('passes and calls next() when relaying from local domain', () => {
|
|
308
|
+
connection.relaying = true
|
|
309
|
+
let code
|
|
310
|
+
plugin.check_sender(
|
|
311
|
+
(c) => {
|
|
312
|
+
code = c
|
|
313
|
+
},
|
|
314
|
+
connection,
|
|
315
|
+
[new Address('<user@test.com>')],
|
|
316
|
+
)
|
|
317
|
+
assert.equal(code, undefined)
|
|
318
|
+
assert.ok(connection.transaction.notes.local_sender)
|
|
319
|
+
const r = connection.transaction.results.get(plugin)
|
|
320
|
+
assert.ok(r.pass.includes('mail_from'))
|
|
321
|
+
})
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
// ─── set_queue ────────────────────────────────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
describe('smtp_forward set_queue', () => {
|
|
327
|
+
let plugin, connection
|
|
328
|
+
|
|
329
|
+
beforeEach(() => {
|
|
330
|
+
plugin = makePlugin()
|
|
331
|
+
connection = makeConnection()
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
it('returns false when transaction has no notes (no transaction)', () => {
|
|
335
|
+
connection.transaction = null
|
|
336
|
+
assert.equal(plugin.set_queue(connection, 'smtp_forward', 'test.com'), false)
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
it('sets queue.wants on first call', () => {
|
|
340
|
+
const result = plugin.set_queue(connection, 'smtp_forward', 'test.com')
|
|
341
|
+
assert.equal(result, true)
|
|
342
|
+
assert.equal(connection.transaction.notes.get('queue.wants'), 'smtp_forward')
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it('sets queue.next_hop when domain has a host', () => {
|
|
346
|
+
plugin.set_queue(connection, 'smtp_forward', 'test.com')
|
|
347
|
+
assert.equal(connection.transaction.notes.get('queue.next_hop'), 'smtp://1.2.3.4')
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
it('does not set next_hop when domain has no host override', () => {
|
|
351
|
+
// test2.com has host=2.3.4.5, so it will set next_hop
|
|
352
|
+
plugin.set_queue(connection, 'smtp_forward', 'test2.com')
|
|
353
|
+
assert.equal(connection.transaction.notes.get('queue.next_hop'), 'smtp://2.3.4.5')
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
it('returns true for undefined domain (no dom_cfg)', () => {
|
|
357
|
+
const result = plugin.set_queue(connection, 'smtp_forward', 'unknown.com')
|
|
358
|
+
assert.equal(result, true)
|
|
359
|
+
assert.equal(connection.transaction.notes.get('queue.wants'), 'smtp_forward')
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
it('returns true when queue already set to same value (no dst_host)', () => {
|
|
363
|
+
connection.transaction.notes.set('queue.wants', 'smtp_forward')
|
|
364
|
+
// unknown.com has no host, so dst_host is just from main (localhost)
|
|
365
|
+
const result = plugin.set_queue(connection, 'smtp_forward', 'unknown.com')
|
|
366
|
+
assert.equal(result, true)
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
it('returns true when next_hop matches existing next_hop', () => {
|
|
370
|
+
connection.transaction.notes.set('queue.wants', 'smtp_forward')
|
|
371
|
+
connection.transaction.notes.set('queue.next_hop', 'smtp://1.2.3.4')
|
|
372
|
+
const result = plugin.set_queue(connection, 'smtp_forward', 'test.com')
|
|
373
|
+
assert.equal(result, true)
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
it('returns true when next_hop already set but no new dst_host', () => {
|
|
377
|
+
connection.transaction.notes.set('queue.wants', 'smtp_forward')
|
|
378
|
+
connection.transaction.notes.set('queue.next_hop', 'smtp://1.2.3.4')
|
|
379
|
+
// unknown.com has no specific host so dst_host comes from main.host='localhost'
|
|
380
|
+
// Actually let's use a domain with no host to test the !dst_host branch
|
|
381
|
+
delete plugin.cfg.main.host
|
|
382
|
+
const result = plugin.set_queue(connection, 'smtp_forward', 'unknown.com')
|
|
383
|
+
assert.equal(result, true)
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
it('returns false when different destination (split transaction)', () => {
|
|
387
|
+
connection.transaction.notes.set('queue.wants', 'smtp_forward')
|
|
388
|
+
connection.transaction.notes.set('queue.next_hop', 'smtp://9.9.9.9')
|
|
389
|
+
// test.com has host=1.2.3.4, which differs from 9.9.9.9
|
|
390
|
+
const result = plugin.set_queue(connection, 'smtp_forward', 'test.com')
|
|
391
|
+
assert.equal(result, false)
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
it('returns false when queue_wanted differs from existing', () => {
|
|
395
|
+
connection.transaction.notes.set('queue.wants', 'outbound')
|
|
396
|
+
const result = plugin.set_queue(connection, 'smtp_forward', 'test.com')
|
|
397
|
+
assert.equal(result, false)
|
|
398
|
+
})
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
// ─── check_recipient ─────────────────────────────────────────────────────────
|
|
402
|
+
|
|
403
|
+
describe('smtp_forward check_recipient', () => {
|
|
404
|
+
let plugin, connection
|
|
405
|
+
|
|
406
|
+
beforeEach(() => {
|
|
407
|
+
plugin = makePlugin()
|
|
408
|
+
connection = makeConnection()
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
it('returns without calling next when no transaction', () => {
|
|
412
|
+
connection.transaction = null
|
|
413
|
+
let called = false
|
|
414
|
+
plugin.check_recipient(
|
|
415
|
+
() => {
|
|
416
|
+
called = true
|
|
417
|
+
},
|
|
418
|
+
connection,
|
|
419
|
+
[new Address('<a@test.com>')],
|
|
420
|
+
)
|
|
421
|
+
assert.equal(called, false)
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
it('skips and calls next for rcpt with no host', () => {
|
|
425
|
+
let code
|
|
426
|
+
const rcpt = new Address('<>')
|
|
427
|
+
plugin.check_recipient(
|
|
428
|
+
(c) => {
|
|
429
|
+
code = c
|
|
430
|
+
},
|
|
431
|
+
connection,
|
|
432
|
+
[rcpt],
|
|
433
|
+
)
|
|
434
|
+
assert.equal(code, undefined)
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
it('uses outbound queue when relaying as local_sender', () => {
|
|
438
|
+
connection.relaying = true
|
|
439
|
+
connection.transaction.notes.local_sender = true
|
|
440
|
+
let code
|
|
441
|
+
plugin.check_recipient(
|
|
442
|
+
(c) => {
|
|
443
|
+
code = c
|
|
444
|
+
},
|
|
445
|
+
connection,
|
|
446
|
+
[new Address('<user@example.com>')],
|
|
447
|
+
)
|
|
448
|
+
assert.equal(code, OK)
|
|
449
|
+
assert.equal(connection.transaction.notes.get('queue.wants'), 'outbound')
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
it('accepts rcpt for a configured domain', () => {
|
|
453
|
+
let code
|
|
454
|
+
plugin.check_recipient(
|
|
455
|
+
(c) => {
|
|
456
|
+
code = c
|
|
457
|
+
},
|
|
458
|
+
connection,
|
|
459
|
+
[new Address('<user@test.com>')],
|
|
460
|
+
)
|
|
461
|
+
assert.equal(code, OK)
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
it('denies softly when set_queue fails for configured domain (split transaction)', () => {
|
|
465
|
+
// First call sets queue.wants to smtp_forward for test.com
|
|
466
|
+
plugin.set_queue(connection, 'smtp_forward', 'test.com')
|
|
467
|
+
// Now change the next_hop so the second call conflicts
|
|
468
|
+
connection.transaction.notes.set('queue.next_hop', 'smtp://9.9.9.9')
|
|
469
|
+
let code
|
|
470
|
+
plugin.check_recipient(
|
|
471
|
+
(c) => {
|
|
472
|
+
code = c
|
|
473
|
+
},
|
|
474
|
+
connection,
|
|
475
|
+
[new Address('<user@test.com>')],
|
|
476
|
+
)
|
|
477
|
+
assert.equal(code, DENYSOFT)
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
it('passes through for unconfigured domain (no route)', () => {
|
|
481
|
+
let code
|
|
482
|
+
plugin.check_recipient(
|
|
483
|
+
(c) => {
|
|
484
|
+
code = c
|
|
485
|
+
},
|
|
486
|
+
connection,
|
|
487
|
+
[new Address('<user@unknown.com>')],
|
|
488
|
+
)
|
|
489
|
+
assert.equal(code, undefined) // next() with no args
|
|
490
|
+
})
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
// ─── auth ─────────────────────────────────────────────────────────────────────
|
|
494
|
+
|
|
495
|
+
describe('smtp_forward auth', () => {
|
|
496
|
+
let plugin, connection, smtp_client
|
|
497
|
+
|
|
498
|
+
beforeEach(() => {
|
|
499
|
+
plugin = makePlugin()
|
|
500
|
+
connection = makeConnection()
|
|
501
|
+
smtp_client = new MockSMTPClient()
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
it('does nothing when smtp_client.secured is pending (false)', () => {
|
|
505
|
+
smtp_client.secured = false
|
|
506
|
+
const cfg = { auth_type: 'plain', auth_user: 'user', auth_pass: 'pass', host: 'relay', port: 25 }
|
|
507
|
+
plugin.auth(cfg, connection, smtp_client)
|
|
508
|
+
smtp_client.emit('capabilities')
|
|
509
|
+
assert.equal(smtp_client.commands.length, 0) // AUTH not sent
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
it('sends AUTH PLAIN credentials when auth_type=plain', () => {
|
|
513
|
+
const cfg = { auth_type: 'plain', auth_user: 'testuser', auth_pass: 'testpass', host: 'relay', port: 25 }
|
|
514
|
+
plugin.auth(cfg, connection, smtp_client)
|
|
515
|
+
smtp_client.emit('capabilities')
|
|
516
|
+
assert.ok(smtp_client.commands.some((c) => /^AUTH PLAIN/.test(c)))
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
it('AUTH PLAIN base64 encodes \\0user\\0pass', () => {
|
|
520
|
+
const cfg = { auth_type: 'plain', auth_user: 'u', auth_pass: 'p', host: 'relay', port: 25 }
|
|
521
|
+
plugin.auth(cfg, connection, smtp_client)
|
|
522
|
+
smtp_client.emit('capabilities')
|
|
523
|
+
const authCmd = smtp_client.commands.find((c) => /^AUTH PLAIN/.test(c))
|
|
524
|
+
assert.ok(authCmd)
|
|
525
|
+
const encoded = authCmd.split(' ')[2]
|
|
526
|
+
assert.equal(Buffer.from(encoded, 'base64').toString(), '\0u\0p')
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
it('sends AUTH LOGIN and sets authenticating=true when auth_type=login', () => {
|
|
530
|
+
const cfg = { auth_type: 'login', auth_user: 'testuser', auth_pass: 'testpass', host: 'relay', port: 25 }
|
|
531
|
+
plugin.auth(cfg, connection, smtp_client)
|
|
532
|
+
smtp_client.emit('capabilities')
|
|
533
|
+
assert.ok(smtp_client.commands.includes('AUTH LOGIN'))
|
|
534
|
+
assert.equal(smtp_client.authenticating, true)
|
|
535
|
+
assert.equal(smtp_client.authenticated, false)
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
it('login: responds to auth_username with base64 username', () => {
|
|
539
|
+
const cfg = { auth_type: 'login', auth_user: 'testuser', auth_pass: 'testpass', host: 'relay', port: 25 }
|
|
540
|
+
plugin.auth(cfg, connection, smtp_client)
|
|
541
|
+
smtp_client.emit('capabilities')
|
|
542
|
+
smtp_client.emit('auth_username')
|
|
543
|
+
assert.equal(smtp_client.commands.at(-1), Buffer.from('testuser').toString('base64'))
|
|
544
|
+
})
|
|
219
545
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
546
|
+
it('login: responds to auth_password with base64 password', () => {
|
|
547
|
+
const cfg = { auth_type: 'login', auth_user: 'testuser', auth_pass: 'testpass', host: 'relay', port: 25 }
|
|
548
|
+
plugin.auth(cfg, connection, smtp_client)
|
|
549
|
+
smtp_client.emit('capabilities')
|
|
550
|
+
smtp_client.emit('auth_password')
|
|
551
|
+
assert.equal(smtp_client.commands.at(-1), Buffer.from('testpass').toString('base64'))
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
it('skips AUTH when secured is undefined (not pending)', () => {
|
|
555
|
+
// secured is undefined → no early return, AUTH PLAIN is sent
|
|
556
|
+
const cfg = { auth_type: 'plain', auth_user: 'u', auth_pass: 'p', host: 'relay', port: 25 }
|
|
557
|
+
delete smtp_client.secured
|
|
558
|
+
plugin.auth(cfg, connection, smtp_client)
|
|
559
|
+
smtp_client.emit('capabilities')
|
|
560
|
+
assert.ok(smtp_client.commands.some((c) => /^AUTH PLAIN/.test(c)))
|
|
561
|
+
})
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
// ─── forward_enabled ──────────────────────────────────────────────────────────
|
|
565
|
+
|
|
566
|
+
describe('smtp_forward forward_enabled', () => {
|
|
567
|
+
let plugin, connection
|
|
568
|
+
|
|
569
|
+
beforeEach(() => {
|
|
570
|
+
plugin = makePlugin()
|
|
571
|
+
connection = makeConnection()
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
it('returns false when queue.wants is set to a non smtp_forward value', () => {
|
|
575
|
+
connection.transaction.notes.set('queue.wants', 'outbound')
|
|
576
|
+
assert.equal(plugin.forward_enabled(connection, plugin.cfg.main), false)
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
it('returns false when relaying and outbound is disabled', () => {
|
|
580
|
+
connection.relaying = true
|
|
581
|
+
// enable_outbound is false by default in test config
|
|
582
|
+
assert.equal(plugin.forward_enabled(connection, plugin.cfg.main), false)
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
it('returns true when queue.wants is smtp_forward', () => {
|
|
586
|
+
connection.transaction.notes.set('queue.wants', 'smtp_forward')
|
|
587
|
+
assert.equal(plugin.forward_enabled(connection, plugin.cfg.main), true)
|
|
588
|
+
})
|
|
589
|
+
|
|
590
|
+
it('returns true when not relaying (even if outbound disabled)', () => {
|
|
591
|
+
connection.relaying = false
|
|
592
|
+
assert.equal(plugin.forward_enabled(connection, plugin.cfg.main), true)
|
|
593
|
+
})
|
|
594
|
+
|
|
595
|
+
it('returns true when relaying and outbound is enabled', () => {
|
|
596
|
+
connection.relaying = true
|
|
597
|
+
plugin.cfg.main.enable_outbound = true
|
|
598
|
+
assert.equal(plugin.forward_enabled(connection, plugin.cfg.main), true)
|
|
599
|
+
})
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
// ─── queue_forward ────────────────────────────────────────────────────────────
|
|
603
|
+
|
|
604
|
+
describe('smtp_forward queue_forward', () => {
|
|
605
|
+
let plugin, connection, restore
|
|
606
|
+
|
|
607
|
+
beforeEach(() => {
|
|
608
|
+
plugin = makePlugin()
|
|
609
|
+
connection = makeConnection()
|
|
610
|
+
connection.transaction.rcpt_to = [new Address('<rcpt@example.com>')]
|
|
611
|
+
connection.transaction.mail_from = new Address('<sender@example.com>')
|
|
612
|
+
connection.relaying = false
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
afterEach(() => {
|
|
616
|
+
if (restore) {
|
|
617
|
+
restore()
|
|
618
|
+
restore = null
|
|
619
|
+
}
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
it('returns without calling next when remote.closed', () => {
|
|
623
|
+
connection.remote.closed = true
|
|
624
|
+
let called = false
|
|
625
|
+
plugin.queue_forward(() => {
|
|
626
|
+
called = true
|
|
627
|
+
}, connection)
|
|
628
|
+
assert.equal(called, false)
|
|
629
|
+
})
|
|
630
|
+
|
|
631
|
+
it('calls next() when forward_enabled returns false', (t, done) => {
|
|
632
|
+
connection.relaying = true // outbound disabled → forward_enabled=false
|
|
633
|
+
plugin.queue_forward((code) => {
|
|
634
|
+
assert.equal(code, undefined)
|
|
635
|
+
done()
|
|
636
|
+
}, connection)
|
|
637
|
+
})
|
|
638
|
+
|
|
639
|
+
it('forwards mail: mail event triggers first RCPT', (t, done) => {
|
|
640
|
+
const client = new MockSMTPClient()
|
|
641
|
+
restore = stubGetClientPlugin((plug, conn, cfg, cb) => cb(null, client))
|
|
642
|
+
|
|
643
|
+
plugin.queue_forward(() => {}, connection)
|
|
644
|
+
client.emit('mail')
|
|
645
|
+
|
|
646
|
+
assert.ok(client.commands.some((c) => /^RCPT TO:/.test(c)))
|
|
647
|
+
done()
|
|
648
|
+
})
|
|
649
|
+
|
|
650
|
+
it('sends DATA after last RCPT TO', (t, done) => {
|
|
651
|
+
const client = new MockSMTPClient()
|
|
652
|
+
restore = stubGetClientPlugin((plug, conn, cfg, cb) => cb(null, client))
|
|
653
|
+
|
|
654
|
+
plugin.queue_forward(() => {}, connection)
|
|
655
|
+
client.emit('mail') // sends RCPT TO for index 0
|
|
656
|
+
client.emit('rcpt') // one_message_per_rcpt=true, sends DATA
|
|
657
|
+
|
|
658
|
+
// wait for the DATA command
|
|
659
|
+
assert.ok(client.commands.some((c) => c === 'DATA' || c.includes('DATA')))
|
|
660
|
+
done()
|
|
661
|
+
})
|
|
662
|
+
|
|
663
|
+
it('data event calls start_data with message_stream', (t, done) => {
|
|
664
|
+
const client = new MockSMTPClient()
|
|
665
|
+
restore = stubGetClientPlugin((plug, conn, cfg, cb) => cb(null, client))
|
|
666
|
+
|
|
667
|
+
plugin.queue_forward(() => {}, connection)
|
|
668
|
+
client.emit('mail')
|
|
669
|
+
client.emit('rcpt') // sends DATA (one_message_per_rcpt)
|
|
670
|
+
client.emit('data')
|
|
671
|
+
|
|
672
|
+
assert.ok(client.started)
|
|
673
|
+
done()
|
|
674
|
+
})
|
|
675
|
+
|
|
676
|
+
it('dot event calls next(OK) and releases when all rcpts done', (t, done) => {
|
|
677
|
+
const client = new MockSMTPClient()
|
|
678
|
+
restore = stubGetClientPlugin((plug, conn, cfg, cb) => cb(null, client))
|
|
679
|
+
|
|
680
|
+
let gotCode
|
|
681
|
+
plugin.queue_forward((code) => {
|
|
682
|
+
gotCode = code
|
|
683
|
+
}, connection)
|
|
684
|
+
|
|
685
|
+
client.emit('mail')
|
|
686
|
+
client.emit('rcpt')
|
|
687
|
+
client.emit('data')
|
|
688
|
+
client.emit('dot')
|
|
689
|
+
|
|
690
|
+
// release() is called after call_next() in the dot handler
|
|
691
|
+
assert.equal(gotCode, OK)
|
|
692
|
+
assert.ok(client.released)
|
|
693
|
+
done()
|
|
694
|
+
})
|
|
695
|
+
|
|
696
|
+
it('dot event sends RSET when more rcpts remain (multi-rcpt, one_message_per_rcpt)', (t, done) => {
|
|
697
|
+
connection.transaction.rcpt_to = [new Address('<a@example.com>'), new Address('<b@example.com>')]
|
|
698
|
+
const client = new MockSMTPClient()
|
|
699
|
+
restore = stubGetClientPlugin((plug, conn, cfg, cb) => cb(null, client))
|
|
700
|
+
|
|
701
|
+
plugin.queue_forward(() => {}, connection)
|
|
702
|
+
client.emit('mail') // sends RCPT TO for index 0
|
|
703
|
+
client.emit('rcpt') // one_message_per_rcpt → sends DATA
|
|
704
|
+
client.emit('data')
|
|
705
|
+
client.commands = [] // clear to observe next commands
|
|
706
|
+
client.emit('dot') // more rcpts remain → RSET
|
|
707
|
+
|
|
708
|
+
assert.ok(client.commands.includes('RSET'))
|
|
709
|
+
done()
|
|
710
|
+
})
|
|
711
|
+
|
|
712
|
+
it('rset event sends MAIL FROM', (t, done) => {
|
|
713
|
+
const client = new MockSMTPClient()
|
|
714
|
+
restore = stubGetClientPlugin((plug, conn, cfg, cb) => cb(null, client))
|
|
715
|
+
|
|
716
|
+
plugin.queue_forward(() => {}, connection)
|
|
717
|
+
client.commands = []
|
|
718
|
+
client.emit('rset')
|
|
719
|
+
|
|
720
|
+
assert.ok(client.commands.some((c) => /^MAIL FROM:/.test(c)))
|
|
721
|
+
done()
|
|
722
|
+
})
|
|
723
|
+
|
|
724
|
+
it('bad_code 5xx emits DENY and releases', (t, done) => {
|
|
725
|
+
const client = new MockSMTPClient()
|
|
726
|
+
restore = stubGetClientPlugin((plug, conn, cfg, cb) => cb(null, client))
|
|
727
|
+
|
|
728
|
+
let gotCode
|
|
729
|
+
plugin.queue_forward((code) => {
|
|
730
|
+
gotCode = code
|
|
731
|
+
}, connection)
|
|
732
|
+
|
|
733
|
+
client.emit('bad_code', '550', 'User unknown')
|
|
734
|
+
|
|
735
|
+
// release() is called after call_next() in the bad_code handler
|
|
736
|
+
assert.equal(gotCode, DENY)
|
|
737
|
+
assert.ok(client.released)
|
|
738
|
+
done()
|
|
739
|
+
})
|
|
740
|
+
|
|
741
|
+
it('bad_code 4xx emits DENYSOFT and releases', (t, done) => {
|
|
742
|
+
const client = new MockSMTPClient()
|
|
743
|
+
restore = stubGetClientPlugin((plug, conn, cfg, cb) => cb(null, client))
|
|
744
|
+
|
|
745
|
+
plugin.queue_forward((code) => {
|
|
746
|
+
assert.equal(code, DENYSOFT)
|
|
747
|
+
done()
|
|
748
|
+
}, connection)
|
|
749
|
+
|
|
750
|
+
client.emit('bad_code', '421', 'Service unavailable')
|
|
751
|
+
})
|
|
752
|
+
|
|
753
|
+
it('dead_sender: adds err result and skips forwarding', (t, done) => {
|
|
754
|
+
const client = new MockSMTPClient()
|
|
755
|
+
client.is_dead_sender = () => true
|
|
756
|
+
restore = stubGetClientPlugin((plug, conn, cfg, cb) => cb(null, client))
|
|
757
|
+
|
|
758
|
+
plugin.queue_forward(() => {}, connection)
|
|
759
|
+
client.emit('mail')
|
|
760
|
+
|
|
761
|
+
const r = connection.transaction.results.get(plugin)
|
|
762
|
+
assert.ok(r.err.some((e) => /dead sender/.test(e)))
|
|
763
|
+
done()
|
|
764
|
+
})
|
|
765
|
+
|
|
766
|
+
it('calls plugin.auth when auth_user is configured in cfg', (t, done) => {
|
|
767
|
+
const client = new MockSMTPClient()
|
|
768
|
+
restore = stubGetClientPlugin((plug, conn, cfg, cb) => cb(null, client))
|
|
769
|
+
|
|
770
|
+
// point the connection to test.com domain which has auth_user in the ini
|
|
771
|
+
connection.transaction.rcpt_to = [new Address('<user@test.com>')]
|
|
772
|
+
|
|
773
|
+
let authCalled = false
|
|
774
|
+
const origAuth = plugin.auth
|
|
775
|
+
plugin.auth = () => {
|
|
776
|
+
authCalled = true
|
|
777
|
+
}
|
|
778
|
+
plugin.queue_forward(() => {}, connection)
|
|
779
|
+
plugin.auth = origAuth
|
|
780
|
+
|
|
781
|
+
assert.ok(authCalled)
|
|
782
|
+
done()
|
|
783
|
+
})
|
|
784
|
+
|
|
785
|
+
it('uses forwarding_host_pool when configured', (t, done) => {
|
|
786
|
+
const client = new MockSMTPClient()
|
|
787
|
+
let capturedCfg
|
|
788
|
+
restore = stubGetClientPlugin((plug, conn, cfg, cb) => {
|
|
789
|
+
capturedCfg = cfg
|
|
790
|
+
cb(null, client)
|
|
226
791
|
})
|
|
792
|
+
|
|
793
|
+
plugin.cfg.main.forwarding_host_pool = '10.0.0.1:25'
|
|
794
|
+
delete plugin.cfg.main.host
|
|
795
|
+
plugin.queue_forward(() => {}, connection)
|
|
796
|
+
|
|
797
|
+
assert.ok(capturedCfg.forwarding_host_pool)
|
|
798
|
+
done()
|
|
799
|
+
})
|
|
800
|
+
})
|
|
801
|
+
|
|
802
|
+
// ─── get_mx_next_hop ─────────────────────────────────────────────────────────
|
|
803
|
+
|
|
804
|
+
describe('smtp_forward get_mx_next_hop', () => {
|
|
805
|
+
it('parses smtp URL with port', () => {
|
|
806
|
+
const mx = smtp_client_mod.smtp_client // not used; just accessing exports
|
|
807
|
+
const plugin = makePlugin()
|
|
808
|
+
const mx_val = plugin.get_mx_next_hop('smtp://10.0.0.1:587')
|
|
809
|
+
assert.equal(mx_val.exchange, '10.0.0.1')
|
|
810
|
+
assert.equal(mx_val.port, '587')
|
|
811
|
+
assert.equal(mx_val.priority, 0)
|
|
812
|
+
})
|
|
813
|
+
|
|
814
|
+
it('defaults port to 25 for smtp without explicit port', () => {
|
|
815
|
+
const plugin = makePlugin()
|
|
816
|
+
const mx_val = plugin.get_mx_next_hop('smtp://10.0.0.1')
|
|
817
|
+
assert.equal(mx_val.port, 25)
|
|
818
|
+
})
|
|
819
|
+
|
|
820
|
+
it('parses lmtp URL and sets using_lmtp=true with port 24', () => {
|
|
821
|
+
const plugin = makePlugin()
|
|
822
|
+
const mx_val = plugin.get_mx_next_hop('lmtp://10.0.0.2')
|
|
823
|
+
assert.equal(mx_val.using_lmtp, true)
|
|
824
|
+
assert.equal(mx_val.port, 24)
|
|
825
|
+
})
|
|
826
|
+
|
|
827
|
+
it('extracts auth credentials from URL', () => {
|
|
828
|
+
const plugin = makePlugin()
|
|
829
|
+
const mx_val = plugin.get_mx_next_hop('smtp://user:secret@10.0.0.1:25')
|
|
830
|
+
assert.equal(mx_val.auth_type, 'plain')
|
|
831
|
+
assert.equal(mx_val.auth_user, 'user')
|
|
832
|
+
assert.equal(mx_val.auth_pass, 'secret')
|
|
833
|
+
})
|
|
834
|
+
})
|
|
835
|
+
|
|
836
|
+
// ─── get_mx ───────────────────────────────────────────────────────────────────
|
|
837
|
+
|
|
838
|
+
describe('smtp_forward get_mx', () => {
|
|
839
|
+
let plugin, hmail
|
|
840
|
+
|
|
841
|
+
beforeEach(() => {
|
|
842
|
+
plugin = makePlugin()
|
|
843
|
+
hmail = makeHmail()
|
|
844
|
+
})
|
|
845
|
+
|
|
846
|
+
it('returns no route for undefined domains', (t, done) => {
|
|
847
|
+
plugin.get_mx(
|
|
848
|
+
(code, mx) => {
|
|
849
|
+
assert.equal(code, undefined)
|
|
850
|
+
assert.equal(mx, undefined)
|
|
851
|
+
done()
|
|
852
|
+
},
|
|
853
|
+
hmail,
|
|
854
|
+
'undefined.com',
|
|
855
|
+
)
|
|
856
|
+
})
|
|
857
|
+
|
|
858
|
+
it('returns no route when queue.wants is not smtp_forward or outbound', (t, done) => {
|
|
859
|
+
hmail.todo.notes.set('queue.wants', 'some_other_queue')
|
|
860
|
+
plugin.get_mx(
|
|
861
|
+
(code, mx) => {
|
|
862
|
+
assert.equal(code, undefined)
|
|
863
|
+
done()
|
|
864
|
+
},
|
|
865
|
+
hmail,
|
|
866
|
+
'test.com',
|
|
867
|
+
)
|
|
868
|
+
})
|
|
869
|
+
|
|
870
|
+
it('returns route from next_hop URL when queue.wants=smtp_forward', (t, done) => {
|
|
871
|
+
hmail.todo.notes.set('queue.wants', 'smtp_forward')
|
|
872
|
+
hmail.todo.notes.set('queue.next_hop', 'smtp://4.3.2.1:465')
|
|
873
|
+
plugin.get_mx(
|
|
874
|
+
(code, mx) => {
|
|
875
|
+
assert.equal(code, OK)
|
|
876
|
+
assert.equal(mx.exchange, '4.3.2.1')
|
|
877
|
+
assert.equal(mx.port, '465')
|
|
878
|
+
done()
|
|
879
|
+
},
|
|
880
|
+
hmail,
|
|
881
|
+
'anything.com',
|
|
882
|
+
)
|
|
883
|
+
})
|
|
884
|
+
|
|
885
|
+
it('returns route for configured domain', (t, done) => {
|
|
886
|
+
plugin.get_mx(
|
|
887
|
+
(code, mx) => {
|
|
888
|
+
assert.equal(code, OK)
|
|
889
|
+
assert.equal(mx.exchange, '1.2.3.4')
|
|
890
|
+
assert.equal(mx.port, 2555)
|
|
891
|
+
assert.equal(mx.auth_user, 'postmaster@test.com')
|
|
892
|
+
assert.equal(mx.auth_pass, 'superDuperSecret')
|
|
893
|
+
done()
|
|
894
|
+
},
|
|
895
|
+
hmail,
|
|
896
|
+
'test.com',
|
|
897
|
+
)
|
|
898
|
+
})
|
|
899
|
+
|
|
900
|
+
it('returns no route (DNS MX) for unconfigured domain when queue.wants=outbound', (t, done) => {
|
|
901
|
+
hmail.todo.notes.set('queue.wants', 'outbound')
|
|
902
|
+
plugin.get_mx(
|
|
903
|
+
(code, mx) => {
|
|
904
|
+
assert.equal(code, undefined)
|
|
905
|
+
done()
|
|
906
|
+
},
|
|
907
|
+
hmail,
|
|
908
|
+
'notconfigured.com',
|
|
909
|
+
)
|
|
910
|
+
})
|
|
911
|
+
|
|
912
|
+
it('uses lmtp URL and sets using_lmtp when next_hop is lmtp', (t, done) => {
|
|
913
|
+
hmail.todo.notes.set('queue.wants', 'smtp_forward')
|
|
914
|
+
hmail.todo.notes.set('queue.next_hop', 'lmtp://4.3.2.1')
|
|
915
|
+
plugin.get_mx(
|
|
916
|
+
(code, mx) => {
|
|
917
|
+
assert.equal(code, OK)
|
|
918
|
+
assert.equal(mx.using_lmtp, true)
|
|
919
|
+
assert.equal(mx.port, 24)
|
|
920
|
+
done()
|
|
921
|
+
},
|
|
922
|
+
hmail,
|
|
923
|
+
'anywhere.com',
|
|
924
|
+
)
|
|
925
|
+
})
|
|
926
|
+
|
|
927
|
+
it('uses mail_from host when domain_selector=mail_from', (t, done) => {
|
|
928
|
+
plugin.cfg.main.domain_selector = 'mail_from'
|
|
929
|
+
hmail.todo.mail_from = new Address('<sender@test.com>')
|
|
930
|
+
plugin.get_mx(
|
|
931
|
+
(code, mx) => {
|
|
932
|
+
assert.equal(code, OK)
|
|
933
|
+
assert.equal(mx.exchange, '1.2.3.4')
|
|
934
|
+
done()
|
|
935
|
+
},
|
|
936
|
+
hmail,
|
|
937
|
+
'anything.com',
|
|
938
|
+
)
|
|
939
|
+
})
|
|
940
|
+
|
|
941
|
+
it('applies mx_opts from domain config', (t, done) => {
|
|
942
|
+
plugin.cfg['test.com'].bind = '192.168.1.1'
|
|
943
|
+
plugin.cfg['test.com'].bind_helo = 'relay.example.com'
|
|
944
|
+
plugin.get_mx(
|
|
945
|
+
(code, mx) => {
|
|
946
|
+
assert.equal(code, OK)
|
|
947
|
+
assert.equal(mx.bind, '192.168.1.1')
|
|
948
|
+
assert.equal(mx.bind_helo, 'relay.example.com')
|
|
949
|
+
done()
|
|
950
|
+
},
|
|
951
|
+
hmail,
|
|
952
|
+
'test.com',
|
|
953
|
+
)
|
|
954
|
+
})
|
|
955
|
+
})
|
|
956
|
+
|
|
957
|
+
// ─── is_outbound_enabled ──────────────────────────────────────────────────────
|
|
958
|
+
|
|
959
|
+
describe('smtp_forward is_outbound_enabled', () => {
|
|
960
|
+
let plugin, connection
|
|
961
|
+
|
|
962
|
+
beforeEach(() => {
|
|
963
|
+
plugin = makePlugin()
|
|
964
|
+
connection = makeConnection()
|
|
965
|
+
})
|
|
966
|
+
|
|
967
|
+
it('enable_outbound is false by default (global)', () => {
|
|
968
|
+
assert.equal(plugin.is_outbound_enabled(plugin.cfg), false)
|
|
969
|
+
})
|
|
970
|
+
|
|
971
|
+
it('per-domain enable_outbound is false by default', () => {
|
|
972
|
+
connection.transaction.rcpt_to = [new Address('<postmaster@test.com>')]
|
|
973
|
+
const cfg = plugin.get_config(connection)
|
|
974
|
+
assert.equal(plugin.is_outbound_enabled(cfg), false)
|
|
975
|
+
})
|
|
976
|
+
|
|
977
|
+
it('per-domain enable_outbound can be set to true', () => {
|
|
978
|
+
plugin.cfg['test.com'].enable_outbound = true
|
|
979
|
+
connection.transaction.rcpt_to = [new Address('<postmaster@test.com>')]
|
|
980
|
+
const cfg = plugin.get_config(connection)
|
|
981
|
+
assert.equal(plugin.is_outbound_enabled(cfg), true)
|
|
982
|
+
})
|
|
983
|
+
|
|
984
|
+
it('per-domain enable_outbound overrides global false', () => {
|
|
985
|
+
plugin.cfg.main.enable_outbound = false
|
|
986
|
+
plugin.cfg['test.com'].enable_outbound = false
|
|
987
|
+
connection.transaction.rcpt_to = [new Address('<postmaster@test.com>')]
|
|
988
|
+
const cfg = plugin.get_config(connection)
|
|
989
|
+
assert.equal(plugin.is_outbound_enabled(cfg), false)
|
|
990
|
+
})
|
|
991
|
+
|
|
992
|
+
it('falls back to global enable_outbound when not in domain cfg', () => {
|
|
993
|
+
plugin.cfg.main.enable_outbound = true
|
|
994
|
+
connection.transaction.rcpt_to = [new Address('<user@example.com>')]
|
|
995
|
+
const cfg = plugin.get_config(connection) // returns cfg.main
|
|
996
|
+
assert.equal(plugin.is_outbound_enabled(cfg), true)
|
|
227
997
|
})
|
|
228
998
|
})
|