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.
- package/CHANGELOG.md +32 -1
- package/CONTRIBUTORS.md +8 -8
- package/Plugins.md +99 -99
- package/config/smtp_forward.ini +10 -0
- package/config/smtp_proxy.ini +10 -0
- package/connection.js +25 -8
- package/docs/plugins/queue/smtp_forward.md +19 -3
- package/docs/plugins/queue/smtp_proxy.md +10 -2
- package/haraka.js +1 -1
- package/outbound/hmail.js +39 -39
- package/outbound/index.js +4 -4
- package/outbound/tls.js +2 -43
- package/package.json +49 -48
- package/plugins/auth/auth_base.js +9 -3
- package/plugins/auth/auth_proxy.js +14 -11
- package/plugins/block_me.js +4 -2
- 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 +12 -4
- package/plugins/queue/smtp_proxy.js +14 -3
- 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 +24 -0
- package/test/outbound/bounce_net_errors.js +3 -2
- 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 +42 -6
- package/test/plugins/queue/smtp_proxy.js +139 -0
- package/test/plugins/reseed_rng.js +34 -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 +45 -12
- package/test/tls_socket.js +82 -0
- package/tls_socket.js +50 -0
|
@@ -28,6 +28,20 @@ exports.load_qmail_queue_ini = function () {
|
|
|
28
28
|
)
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
// qmail-queue envelope: F<sender>\0 (T<rcpt>\0)* \0
|
|
32
|
+
// Built dynamically, sized to exactly the bytes needed.
|
|
33
|
+
// doesn't emit zero padding after the terminating NUL.
|
|
34
|
+
// encodes non-ASCII (SMTPUTF8) addresses correctly
|
|
35
|
+
exports.build_envelope = function (transaction) {
|
|
36
|
+
const NUL = Buffer.from([0])
|
|
37
|
+
const parts = [Buffer.from('F'), Buffer.from(transaction.mail_from.address), NUL]
|
|
38
|
+
for (const rcpt of transaction.rcpt_to) {
|
|
39
|
+
parts.push(Buffer.from('T'), Buffer.from(rcpt.address), NUL)
|
|
40
|
+
}
|
|
41
|
+
parts.push(NUL)
|
|
42
|
+
return Buffer.concat(parts)
|
|
43
|
+
}
|
|
44
|
+
|
|
31
45
|
exports.hook_queue = function (next, connection) {
|
|
32
46
|
const plugin = this
|
|
33
47
|
|
|
@@ -72,25 +86,7 @@ exports.hook_queue = function (next, connection) {
|
|
|
72
86
|
return
|
|
73
87
|
}
|
|
74
88
|
plugin.loginfo('Message Stream sent to qmail. Now sending envelope')
|
|
75
|
-
|
|
76
|
-
// Hope this will be big enough...
|
|
77
|
-
const buf = Buffer.alloc(4096)
|
|
78
|
-
let p = 0
|
|
79
|
-
buf[p++] = 70
|
|
80
|
-
const mail_from = connection.transaction.mail_from.address()
|
|
81
|
-
for (let i = 0; i < mail_from.length; i++) {
|
|
82
|
-
buf[p++] = mail_from.charCodeAt(i)
|
|
83
|
-
}
|
|
84
|
-
buf[p++] = 0
|
|
85
|
-
connection.transaction.rcpt_to.forEach((rcpt) => {
|
|
86
|
-
buf[p++] = 84
|
|
87
|
-
const rcpt_to = rcpt.address()
|
|
88
|
-
for (let j = 0; j < rcpt_to.length; j++) {
|
|
89
|
-
buf[p++] = rcpt_to.charCodeAt(j)
|
|
90
|
-
}
|
|
91
|
-
buf[p++] = 0
|
|
92
|
-
})
|
|
93
|
-
buf[p++] = 0
|
|
89
|
+
const buf = plugin.build_envelope(connection.transaction)
|
|
94
90
|
qmail_queue.stdout.on('error', (err) => {}) // stdout throws an error on close
|
|
95
91
|
qmail_queue.stdout.end(buf)
|
|
96
92
|
})
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
const url = require('node:url')
|
|
8
8
|
|
|
9
9
|
const smtp_client_mod = require('../../smtp_client')
|
|
10
|
+
const tls_socket = require('../../tls_socket')
|
|
10
11
|
|
|
11
12
|
exports.register = function () {
|
|
12
13
|
this.load_errs = []
|
|
@@ -46,12 +47,19 @@ exports.load_smtp_forward_ini = function () {
|
|
|
46
47
|
'-main.check_recipient',
|
|
47
48
|
'*.enable_tls',
|
|
48
49
|
'*.enable_outbound',
|
|
50
|
+
'+tls.requestCert',
|
|
51
|
+
'+tls.honorCipherOrder',
|
|
52
|
+
'-tls.rejectUnauthorized',
|
|
49
53
|
],
|
|
50
54
|
},
|
|
51
55
|
() => {
|
|
52
56
|
this.load_smtp_forward_ini()
|
|
53
57
|
},
|
|
54
58
|
)
|
|
59
|
+
|
|
60
|
+
// Build backend TLS options from tls.ini [main] + this plugin's [tls] section.
|
|
61
|
+
// Re-derived on every (re)load so SIGHUP picks up edits.
|
|
62
|
+
this.tls_options = tls_socket.load_plugin_tls_options(this.cfg.tls || {})
|
|
55
63
|
}
|
|
56
64
|
|
|
57
65
|
exports.get_config = function (conn) {
|
|
@@ -264,7 +272,7 @@ exports.queue_forward = function (next, connection) {
|
|
|
264
272
|
smtp_client.send_command('DATA')
|
|
265
273
|
return
|
|
266
274
|
}
|
|
267
|
-
smtp_client.send_command('RCPT', `TO:${txn.rcpt_to[rcpt].format(!smtp_client.
|
|
275
|
+
smtp_client.send_command('RCPT', `TO:${txn.rcpt_to[rcpt].format(!smtp_client.smtputf8)}`)
|
|
268
276
|
rcpt++
|
|
269
277
|
}
|
|
270
278
|
|
|
@@ -354,10 +362,10 @@ exports.get_mx = function (next, hmail, domain) {
|
|
|
354
362
|
}
|
|
355
363
|
|
|
356
364
|
// apply auth/mx options
|
|
357
|
-
|
|
358
|
-
if (cfg[o] === undefined)
|
|
365
|
+
for (const o of mx_opts) {
|
|
366
|
+
if (cfg[o] === undefined) continue
|
|
359
367
|
mx[o] = this.cfg[dom][o]
|
|
360
|
-
}
|
|
368
|
+
}
|
|
361
369
|
|
|
362
370
|
next(OK, mx)
|
|
363
371
|
}
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// and passes back any errors seen on the ongoing server to the
|
|
5
5
|
// originating server.
|
|
6
6
|
|
|
7
|
-
const smtp_client_mod = require('
|
|
7
|
+
const smtp_client_mod = require('../../smtp_client')
|
|
8
|
+
const tls_socket = require('../../tls_socket')
|
|
8
9
|
|
|
9
10
|
exports.register = function () {
|
|
10
11
|
this.load_smtp_proxy_ini()
|
|
@@ -18,13 +19,23 @@ exports.load_smtp_proxy_ini = function () {
|
|
|
18
19
|
this.cfg = this.config.get(
|
|
19
20
|
'smtp_proxy.ini',
|
|
20
21
|
{
|
|
21
|
-
booleans: [
|
|
22
|
+
booleans: [
|
|
23
|
+
'-main.enable_tls',
|
|
24
|
+
'+main.enable_outbound',
|
|
25
|
+
'+tls.requestCert',
|
|
26
|
+
'+tls.honorCipherOrder',
|
|
27
|
+
'-tls.rejectUnauthorized',
|
|
28
|
+
],
|
|
22
29
|
},
|
|
23
30
|
() => {
|
|
24
31
|
this.load_smtp_proxy_ini()
|
|
25
32
|
},
|
|
26
33
|
)
|
|
27
34
|
|
|
35
|
+
// Build backend TLS options from tls.ini [main] + this plugin's [tls] section.
|
|
36
|
+
// Re-derived on every (re)load so SIGHUP picks up edits.
|
|
37
|
+
this.tls_options = tls_socket.load_plugin_tls_options(this.cfg.tls || {})
|
|
38
|
+
|
|
28
39
|
if (this.cfg.main.enable_outbound) {
|
|
29
40
|
this.lognotice('outbound enabled, will default to disabled in Haraka v3 (see #1472)')
|
|
30
41
|
}
|
|
@@ -82,7 +93,7 @@ exports.hook_rcpt_ok = (next, connection, recipient) => {
|
|
|
82
93
|
return
|
|
83
94
|
}
|
|
84
95
|
smtp_client.next = next
|
|
85
|
-
smtp_client.send_command('RCPT', `TO:${recipient.format(!smtp_client.
|
|
96
|
+
smtp_client.send_command('RCPT', `TO:${recipient.format(!smtp_client.smtputf8)}`)
|
|
86
97
|
}
|
|
87
98
|
|
|
88
99
|
exports.hook_data = (next, connection) => {
|
package/plugins/tls.js
CHANGED
|
@@ -85,6 +85,14 @@ exports.upgrade_connection = function (next, connection, params) {
|
|
|
85
85
|
/* Watch for STARTTLS directive from client. */
|
|
86
86
|
if (params[0].toUpperCase() !== 'STARTTLS') return next()
|
|
87
87
|
|
|
88
|
+
// RFC 3207 §4: discard any plaintext the client pipelined after
|
|
89
|
+
// STARTTLS. Otherwise connection.respond() restores state to CMD and
|
|
90
|
+
// re-enters _process_data(), parsing and executing those buffered
|
|
91
|
+
// cleartext commands before the TLS handshake completes (STARTTLS
|
|
92
|
+
// command injection). Mirrors the buffer-nuke already used for
|
|
93
|
+
// SSL-over-plaintext detection in connection.process_line().
|
|
94
|
+
connection.current_data = null
|
|
95
|
+
|
|
88
96
|
/* Respond to STARTTLS command. */
|
|
89
97
|
connection.respond(220, 'Go ahead.')
|
|
90
98
|
|
|
@@ -109,7 +117,7 @@ exports.upgrade_connection = function (next, connection, params) {
|
|
|
109
117
|
connection.notes.cleanUpDisconnect = nextOnce
|
|
110
118
|
|
|
111
119
|
/* Upgrade the connection to TLS. */
|
|
112
|
-
connection.client.upgrade((verified,
|
|
120
|
+
connection.client.upgrade((verified, verifyError, cert, cipher) => {
|
|
113
121
|
if (called_next) return
|
|
114
122
|
clearTimeout(connection.notes.tls_timer)
|
|
115
123
|
called_next = true
|
|
@@ -117,12 +125,12 @@ exports.upgrade_connection = function (next, connection, params) {
|
|
|
117
125
|
connection.setTLS({
|
|
118
126
|
cipher,
|
|
119
127
|
verified,
|
|
120
|
-
|
|
128
|
+
verifyError,
|
|
121
129
|
peerCertificate: cert,
|
|
122
130
|
})
|
|
123
131
|
|
|
124
132
|
connection.results.add(plugin, connection.tls)
|
|
125
|
-
plugin.emit_upgrade_msg(connection, verified,
|
|
133
|
+
plugin.emit_upgrade_msg(connection, verified, verifyError, cert, cipher)
|
|
126
134
|
next(OK)
|
|
127
135
|
})
|
|
128
136
|
})
|
|
@@ -135,13 +143,13 @@ exports.hook_disconnect = (next, connection) => {
|
|
|
135
143
|
next()
|
|
136
144
|
}
|
|
137
145
|
|
|
138
|
-
exports.emit_upgrade_msg = function (conn, verified,
|
|
146
|
+
exports.emit_upgrade_msg = function (conn, verified, verifyError, cert, cipher) {
|
|
139
147
|
let msg = 'secured:'
|
|
140
148
|
if (cipher) {
|
|
141
149
|
msg += ` cipher=${cipher.name} version=${cipher.version}`
|
|
142
150
|
}
|
|
143
151
|
msg += ` verified=${verified}`
|
|
144
|
-
if (
|
|
152
|
+
if (verifyError) msg += ` error="${verifyError}"`
|
|
145
153
|
if (cert) {
|
|
146
154
|
if (cert.subject) {
|
|
147
155
|
msg += ` cn="${cert.subject.CN}" organization="${cert.subject.O}"`
|
package/plugins/xclient.js
CHANGED
|
@@ -110,7 +110,9 @@ exports.hook_unrecognized_command = function (next, connection, params) {
|
|
|
110
110
|
connection.set('remote.login', xclient.login ? xclient.login : undefined)
|
|
111
111
|
connection.set('hello.host', xclient.helo ? xclient.helo : undefined)
|
|
112
112
|
connection.set('local.ip', xclient.destaddr ? xclient.destaddr : undefined)
|
|
113
|
-
|
|
113
|
+
// parseInt so downstream numeric checks (e.g. the 587/465 auth-required
|
|
114
|
+
// gate in connection.cmd_mail) work; matches the PROXY path.
|
|
115
|
+
connection.set('local.port', xclient.destport ? parseInt(xclient.destport, 10) : undefined)
|
|
114
116
|
if (xclient.proto) {
|
|
115
117
|
connection.set('hello', 'verb', xclient.proto === 'esmtp' ? 'EHLO' : 'HELO')
|
|
116
118
|
}
|
package/server.js
CHANGED
|
@@ -175,7 +175,7 @@ Server._graceful = async (shutdown) => {
|
|
|
175
175
|
const todo = []
|
|
176
176
|
|
|
177
177
|
for (const id of Object.keys(cluster.workers)) {
|
|
178
|
-
todo.push((
|
|
178
|
+
todo.push(() => {
|
|
179
179
|
return new Promise((resolve) => {
|
|
180
180
|
Server.lognotice(`Killing worker: ${id}`)
|
|
181
181
|
const worker = cluster.workers[id]
|
|
@@ -223,8 +223,10 @@ Server._graceful = async (shutdown) => {
|
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
while (todo.length) {
|
|
226
|
-
// process batches of workers
|
|
227
|
-
await
|
|
226
|
+
// process batches of workers: invoke each queued thunk so we
|
|
227
|
+
// actually await the worker shutdown promises (passing the bare
|
|
228
|
+
// functions to Promise.all would resolve immediately).
|
|
229
|
+
await Promise.all(todo.splice(0, limit).map((fn) => fn()))
|
|
228
230
|
}
|
|
229
231
|
|
|
230
232
|
if (shutdown) {
|
package/smtp_client.js
CHANGED
|
@@ -192,7 +192,7 @@ class SMTPClient extends events.EventEmitter {
|
|
|
192
192
|
client.socket.on('end', closed('ended'))
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
-
|
|
195
|
+
load_tls_options(opts = {}) {
|
|
196
196
|
this.tls_options = { servername: this.host, ...opts }
|
|
197
197
|
}
|
|
198
198
|
|
|
@@ -312,8 +312,8 @@ exports.onCapabilitiesOutbound = (smtp_client, secured, connection, config, on_s
|
|
|
312
312
|
// Check if there are any banned TLS hosts
|
|
313
313
|
if (smtp_client.tls_options.no_tls_hosts) {
|
|
314
314
|
// If there are check if these hosts are in the blacklist
|
|
315
|
-
hostBanned = net_utils.ip_in_list(smtp_client.
|
|
316
|
-
serverBanned = net_utils.ip_in_list(smtp_client.
|
|
315
|
+
hostBanned = net_utils.ip_in_list(smtp_client.tls_options.no_tls_hosts, config.host)
|
|
316
|
+
serverBanned = net_utils.ip_in_list(smtp_client.tls_options.no_tls_hosts, smtp_client.remote_ip)
|
|
317
317
|
}
|
|
318
318
|
|
|
319
319
|
if (!hostBanned && !serverBanned && config.enable_tls) {
|
|
@@ -358,7 +358,7 @@ exports.get_client_plugin = (plugin, connection, c, callback) => {
|
|
|
358
358
|
|
|
359
359
|
let secured = false
|
|
360
360
|
|
|
361
|
-
smtp_client.
|
|
361
|
+
smtp_client.load_tls_options(plugin.tls_options)
|
|
362
362
|
|
|
363
363
|
smtp_client.call_next = function (retval, msg) {
|
|
364
364
|
if (this.next) {
|
|
@@ -369,7 +369,10 @@ exports.get_client_plugin = (plugin, connection, c, callback) => {
|
|
|
369
369
|
}
|
|
370
370
|
|
|
371
371
|
smtp_client.on('client_protocol', (line) => {
|
|
372
|
-
|
|
372
|
+
// Don't leak SASL credentials (e.g. AUTH PLAIN <base64>) into
|
|
373
|
+
// protocol logs.
|
|
374
|
+
const safe = String(line).replace(/^(AUTH\s+\S+\s+).+$/i, '$1[redacted]')
|
|
375
|
+
connection.logprotocol(plugin, `C: ${safe}`)
|
|
373
376
|
})
|
|
374
377
|
|
|
375
378
|
smtp_client.on('server_protocol', (line) => {
|
|
@@ -401,21 +404,27 @@ exports.get_client_plugin = (plugin, connection, c, callback) => {
|
|
|
401
404
|
if (!c.auth || smtp_client.authenticated) {
|
|
402
405
|
if (smtp_client.is_dead_sender(plugin, connection)) return
|
|
403
406
|
|
|
404
|
-
smtp_client.send_command('MAIL', `FROM:${connection.transaction.mail_from.format(!smtp_client.
|
|
407
|
+
smtp_client.send_command('MAIL', `FROM:${connection.transaction.mail_from.format(!smtp_client.smtputf8)}`)
|
|
405
408
|
return
|
|
406
409
|
}
|
|
407
410
|
|
|
408
411
|
if (c.auth.type === null || typeof c.auth.type === 'undefined') return // Ignore blank
|
|
409
412
|
const auth_type = c.auth.type.toLowerCase()
|
|
413
|
+
// This listener runs from the socket line handler; an uncaught
|
|
414
|
+
// throw here crashes the forwarding worker. Route failures
|
|
415
|
+
// through the existing smtp_client 'error' flow (logwarn +
|
|
416
|
+
// call_next) so a misconfigured/hostile upstream degrades to a
|
|
417
|
+
// normal SMTP error path instead.
|
|
410
418
|
if (!smtp_client.auth_capabilities.includes(auth_type)) {
|
|
411
|
-
|
|
419
|
+
return smtp_client.emit(
|
|
420
|
+
'error',
|
|
412
421
|
`Auth type "${auth_type}" not supported by server (supports: ${smtp_client.auth_capabilities.join(',')})`,
|
|
413
422
|
)
|
|
414
423
|
}
|
|
415
424
|
switch (auth_type) {
|
|
416
425
|
case 'plain':
|
|
417
426
|
if (!c.auth.user || !c.auth.pass) {
|
|
418
|
-
|
|
427
|
+
return smtp_client.emit('error', 'Must include auth.user and auth.pass for PLAIN auth.')
|
|
419
428
|
}
|
|
420
429
|
logger.debug(`[smtp_client] uuid=${smtp_client.uuid} authenticating as "${c.auth.user}"`)
|
|
421
430
|
smtp_client.send_command(
|
|
@@ -424,9 +433,9 @@ exports.get_client_plugin = (plugin, connection, c, callback) => {
|
|
|
424
433
|
)
|
|
425
434
|
break
|
|
426
435
|
case 'cram-md5':
|
|
427
|
-
|
|
436
|
+
return smtp_client.emit('error', `AUTH ${auth_type} not implemented`)
|
|
428
437
|
default:
|
|
429
|
-
|
|
438
|
+
return smtp_client.emit('error', `Unknown AUTH type: ${auth_type}`)
|
|
430
439
|
}
|
|
431
440
|
})
|
|
432
441
|
|
|
@@ -437,7 +446,7 @@ exports.get_client_plugin = (plugin, connection, c, callback) => {
|
|
|
437
446
|
if (smtp_client.is_dead_sender(plugin, connection)) return
|
|
438
447
|
|
|
439
448
|
smtp_client.authenticated = true
|
|
440
|
-
smtp_client.send_command('MAIL', `FROM:${connection.transaction.mail_from.format(!smtp_client.
|
|
449
|
+
smtp_client.send_command('MAIL', `FROM:${connection.transaction.mail_from.format(!smtp_client.smtputf8)}`)
|
|
441
450
|
})
|
|
442
451
|
|
|
443
452
|
// these errors only get thrown when the connection is still active
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
blocklist@example.com
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sender@example.com
|
package/test/connection.js
CHANGED
|
@@ -676,4 +676,28 @@ describe('connection', () => {
|
|
|
676
676
|
assert.equal(this.connection.transaction.data_bytes, 0)
|
|
677
677
|
})
|
|
678
678
|
})
|
|
679
|
+
|
|
680
|
+
describe('header injection', () => {
|
|
681
|
+
beforeEach(setUp)
|
|
682
|
+
|
|
683
|
+
it('cmd_helo rejects control chars in the host', () => {
|
|
684
|
+
const r = this.connection.cmd_helo('evil\rINJECTED')
|
|
685
|
+
assert.match(String(r), /^501/)
|
|
686
|
+
assert.equal(this.connection.hello.host, null)
|
|
687
|
+
})
|
|
688
|
+
|
|
689
|
+
it('cmd_ehlo rejects control chars in the host', () => {
|
|
690
|
+
const r = this.connection.cmd_ehlo('evil\r\nINJECTED')
|
|
691
|
+
assert.match(String(r), /^501/)
|
|
692
|
+
assert.equal(this.connection.hello.host, null)
|
|
693
|
+
})
|
|
694
|
+
|
|
695
|
+
it('auth_results strips CR/LF so it cannot inject a header', () => {
|
|
696
|
+
const out = this.connection.auth_results('auth=fail smtp.auth=evil\r\nInjected-Header: pwned')
|
|
697
|
+
assert.ok(!out.includes('\r\nInjected-Header:'))
|
|
698
|
+
// the only CRLF present is the legitimate folding (;\r\n\t)
|
|
699
|
+
assert.equal(out.replace(/;\r\n\t/g, '').includes('\r'), false)
|
|
700
|
+
assert.equal(out.replace(/;\r\n\t/g, '').includes('\n'), false)
|
|
701
|
+
})
|
|
702
|
+
})
|
|
679
703
|
})
|
|
@@ -98,9 +98,10 @@ const testCases = [
|
|
|
98
98
|
trigger: (h) => HMailItem.prototype.get_mx_error.apply(h, [{ code: 'SOME-OTHER-ERR' }, {}]),
|
|
99
99
|
},
|
|
100
100
|
{
|
|
101
|
-
|
|
101
|
+
// RFC 7505 NULL MX → DSN.addr_null_mx → 5.1.10 (per haraka-dsn).
|
|
102
|
+
name: 'found_mx with NULL MX (RFC 7505) triggers bounce with dsn_status 5.1.10',
|
|
102
103
|
method: 'bounce',
|
|
103
|
-
status: '5.1.
|
|
104
|
+
status: '5.1.10',
|
|
104
105
|
trigger: (h) => HMailItem.prototype.found_mx.apply(h, [[{ priority: 0, exchange: '' }]]),
|
|
105
106
|
},
|
|
106
107
|
{
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert')
|
|
4
|
+
const { describe, it, beforeEach } = require('node:test')
|
|
5
|
+
|
|
6
|
+
const fixtures = require('haraka-test-fixtures')
|
|
7
|
+
|
|
8
|
+
describe('auth/auth_bridge', () => {
|
|
9
|
+
let plugin
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
plugin = new fixtures.plugin('auth/auth_bridge')
|
|
13
|
+
plugin.load_flat_ini()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
describe('load_flat_ini', () => {
|
|
17
|
+
it('loads smtp_bridge.ini config', () => {
|
|
18
|
+
assert.ok(plugin.cfg)
|
|
19
|
+
assert.ok(plugin.cfg.main)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('cfg.main.host defaults to localhost', () => {
|
|
23
|
+
assert.equal(plugin.cfg.main.host, 'localhost')
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('check_plain_passwd', () => {
|
|
28
|
+
let conn
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
conn = fixtures.connection.createConnection()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('calls try_auth_proxy with just host when no port configured', (t, done) => {
|
|
35
|
+
plugin.cfg.main = { host: 'mail.example.com' }
|
|
36
|
+
plugin.try_auth_proxy = (connection, host, user, passwd, cb) => {
|
|
37
|
+
assert.equal(host, 'mail.example.com')
|
|
38
|
+
assert.equal(user, 'testuser')
|
|
39
|
+
assert.equal(passwd, 'testpass')
|
|
40
|
+
cb(true)
|
|
41
|
+
}
|
|
42
|
+
plugin.check_plain_passwd(conn, 'testuser', 'testpass', (result) => {
|
|
43
|
+
assert.equal(result, true)
|
|
44
|
+
done()
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('calls try_auth_proxy with host:port when port is configured', (t, done) => {
|
|
49
|
+
plugin.cfg.main = { host: 'mail.example.com', port: '587' }
|
|
50
|
+
plugin.try_auth_proxy = (connection, host, user, passwd, cb) => {
|
|
51
|
+
assert.equal(host, 'mail.example.com:587')
|
|
52
|
+
cb(true)
|
|
53
|
+
}
|
|
54
|
+
plugin.check_plain_passwd(conn, 'testuser', 'testpass', (result) => {
|
|
55
|
+
assert.equal(result, true)
|
|
56
|
+
done()
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('passes authentication failure through to callback', (t, done) => {
|
|
61
|
+
plugin.cfg.main = { host: 'mail.example.com' }
|
|
62
|
+
plugin.try_auth_proxy = (connection, host, user, passwd, cb) => {
|
|
63
|
+
cb(false)
|
|
64
|
+
}
|
|
65
|
+
plugin.check_plain_passwd(conn, 'baduser', 'badpass', (result) => {
|
|
66
|
+
assert.equal(result, false)
|
|
67
|
+
done()
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('passes the connection object to try_auth_proxy', (t, done) => {
|
|
72
|
+
plugin.cfg.main = { host: 'mail.example.com' }
|
|
73
|
+
plugin.try_auth_proxy = (connection, host, user, passwd, cb) => {
|
|
74
|
+
assert.equal(connection, conn)
|
|
75
|
+
cb(true)
|
|
76
|
+
}
|
|
77
|
+
plugin.check_plain_passwd(conn, 'user', 'pass', () => done())
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
})
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert')
|
|
4
|
+
const { describe, it, beforeEach } = require('node:test')
|
|
5
|
+
|
|
6
|
+
const fixtures = require('haraka-test-fixtures')
|
|
7
|
+
|
|
8
|
+
function makeConnection(opts = {}) {
|
|
9
|
+
const conn = fixtures.connection.createConnection()
|
|
10
|
+
conn.capabilities = []
|
|
11
|
+
conn.notes.allowed_auth_methods = []
|
|
12
|
+
conn.remote = { is_private: opts.is_private ?? false }
|
|
13
|
+
conn.tls = { enabled: opts.tls_enabled ?? false }
|
|
14
|
+
return conn
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('auth/flat_file', () => {
|
|
18
|
+
let plugin
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
plugin = new fixtures.plugin('auth/flat_file')
|
|
22
|
+
plugin.inherits('auth/auth_base')
|
|
23
|
+
plugin.load_flat_ini()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('load_flat_ini', () => {
|
|
27
|
+
it('populates cfg.users as an object', () => {
|
|
28
|
+
assert.ok(typeof plugin.cfg.users === 'object')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('cfg.users defaults to empty object when not configured', () => {
|
|
32
|
+
// default config has no real users
|
|
33
|
+
assert.deepEqual(plugin.cfg.users, {})
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('hook_capabilities', () => {
|
|
38
|
+
let conn
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
conn = makeConnection()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('skips for public non-TLS connection', (t, done) => {
|
|
45
|
+
plugin.hook_capabilities((rc) => {
|
|
46
|
+
assert.equal(rc, undefined)
|
|
47
|
+
assert.equal(conn.capabilities.length, 0)
|
|
48
|
+
done()
|
|
49
|
+
}, conn)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('adds AUTH methods for private connection (non-TLS)', (t, done) => {
|
|
53
|
+
conn.remote.is_private = true
|
|
54
|
+
plugin.cfg.core.methods = 'PLAIN,LOGIN'
|
|
55
|
+
plugin.hook_capabilities((rc) => {
|
|
56
|
+
assert.equal(rc, undefined)
|
|
57
|
+
assert.ok(
|
|
58
|
+
conn.capabilities.some((c) => c.startsWith('AUTH ')),
|
|
59
|
+
'AUTH capability should be present',
|
|
60
|
+
)
|
|
61
|
+
done()
|
|
62
|
+
}, conn)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('adds AUTH methods when TLS is enabled', (t, done) => {
|
|
66
|
+
conn.tls.enabled = true
|
|
67
|
+
plugin.cfg.core.methods = 'PLAIN,LOGIN'
|
|
68
|
+
plugin.hook_capabilities((rc) => {
|
|
69
|
+
assert.equal(rc, undefined)
|
|
70
|
+
assert.ok(conn.capabilities.some((c) => c.startsWith('AUTH ')))
|
|
71
|
+
done()
|
|
72
|
+
}, conn)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('sets allowed_auth_methods on connection notes', (t, done) => {
|
|
76
|
+
conn.tls.enabled = true
|
|
77
|
+
plugin.cfg.core.methods = 'PLAIN,LOGIN'
|
|
78
|
+
plugin.hook_capabilities(() => {
|
|
79
|
+
assert.deepEqual(conn.notes.allowed_auth_methods, ['PLAIN', 'LOGIN'])
|
|
80
|
+
done()
|
|
81
|
+
}, conn)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('does not add AUTH when no methods configured', (t, done) => {
|
|
85
|
+
conn.tls.enabled = true
|
|
86
|
+
plugin.cfg.core.methods = null
|
|
87
|
+
plugin.hook_capabilities(() => {
|
|
88
|
+
assert.equal(conn.capabilities.length, 0)
|
|
89
|
+
done()
|
|
90
|
+
}, conn)
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
describe('get_plain_passwd', () => {
|
|
95
|
+
beforeEach(() => {
|
|
96
|
+
plugin.cfg.users = { alice: 'secret', bob: 'hunter2' }
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('returns password for known user', (t, done) => {
|
|
100
|
+
plugin.get_plain_passwd('alice', {}, (pw) => {
|
|
101
|
+
assert.equal(pw, 'secret')
|
|
102
|
+
done()
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('calls cb with no args for unknown user', (t, done) => {
|
|
107
|
+
plugin.get_plain_passwd('unknown', {}, (pw) => {
|
|
108
|
+
assert.equal(pw, undefined)
|
|
109
|
+
done()
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('handles multiple users', (t, done) => {
|
|
114
|
+
plugin.get_plain_passwd('bob', {}, (pw) => {
|
|
115
|
+
assert.equal(pw, 'hunter2')
|
|
116
|
+
done()
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('coerces password to string via toString()', (t, done) => {
|
|
121
|
+
plugin.cfg.users.numericuser = 12345
|
|
122
|
+
plugin.get_plain_passwd('numericuser', {}, (pw) => {
|
|
123
|
+
assert.equal(pw, '12345')
|
|
124
|
+
done()
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
})
|