Haraka 3.2.1 → 3.3.1
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/.githooks/pre-commit +41 -0
- package/.prettierignore +1 -0
- package/.qlty/.gitignore +7 -0
- package/.qlty/configs/.shellcheckrc +1 -0
- package/.qlty/qlty.toml +15 -0
- package/CHANGELOG.md +29 -5
- package/CONTRIBUTORS.md +5 -5
- package/README.md +6 -3
- package/bin/haraka +12 -4
- package/config/connection.ini +6 -0
- package/connection.js +67 -68
- package/contrib/bsd-rc.d/haraka +2 -0
- package/docs/CoreConfig.md +2 -0
- package/docs/HAProxy.md +4 -1
- package/eslint.config.mjs +2 -30
- package/haraka.js +2 -2
- package/line_socket.js +6 -33
- package/outbound/hmail.js +18 -29
- package/outbound/index.js +3 -3
- package/outbound/queue.js +8 -5
- package/package.json +49 -46
- package/plugins/auth/auth_proxy.js +7 -4
- package/plugins/block_me.js +1 -1
- package/plugins/delay_deny.js +1 -1
- package/plugins/queue/qmail-queue.js +1 -1
- package/plugins/queue/quarantine.js +5 -5
- package/plugins/queue/smtp_bridge.js +1 -1
- package/plugins/queue/smtp_proxy.js +2 -2
- package/plugins/status.js +2 -2
- package/plugins/toobusy.js +1 -1
- package/plugins.js +4 -3
- package/server.js +172 -28
- package/smtp_client.js +2 -1
- package/test/connection.js +119 -2
- package/test/fixtures/haproxy_allowed/config/connection.ini +3 -0
- package/test/fixtures/haproxy_disabled/config/connection.ini +3 -0
- package/test/fixtures/haproxy_untrusted/config/connection.ini +3 -0
- package/test/fixtures/line_socket.js +1 -1
- package/test/fixtures/util_hmailitem.js +2 -3
- package/test/outbound/index.js +6 -7
- package/test/outbound/qfile.js +1 -1
- package/test/outbound/queue.js +2 -2
- package/test/plugins/auth/auth_base.js +17 -17
- package/test/plugins/auth/auth_bridge.js +3 -3
- package/test/plugins/auth/auth_vpopmaild.js +3 -3
- package/test/plugins/auth/flat_file.js +16 -21
- package/test/plugins/block_me.js +7 -23
- package/test/plugins/data.signatures.js +17 -20
- package/test/plugins/delay_deny.js +3 -4
- package/test/plugins/prevent_credential_leaks.js +17 -21
- package/test/plugins/process_title.js +12 -6
- package/test/plugins/queue/deliver.js +7 -8
- package/test/plugins/queue/discard.js +3 -4
- package/test/plugins/queue/lmtp.js +5 -6
- package/test/plugins/queue/qmail-queue.js +7 -8
- package/test/plugins/queue/quarantine.js +3 -4
- package/test/plugins/queue/smtp_bridge.js +5 -7
- package/test/plugins/queue/smtp_forward.js +49 -60
- package/test/plugins/queue/smtp_proxy.js +6 -7
- package/test/plugins/rcpt_to.host_list_base.js +6 -9
- package/test/plugins/rcpt_to.in_host_list.js +6 -11
- package/test/plugins/record_envelope_addresses.js +33 -60
- package/test/plugins/reseed_rng.js +3 -3
- package/test/plugins/status.js +4 -5
- package/test/plugins/tarpit.js +3 -4
- package/test/plugins/tls.js +3 -5
- package/test/plugins/toobusy.js +186 -9
- package/test/plugins/xclient.js +7 -4
- package/test/server.js +425 -1
- package/test/smtp_client.js +11 -18
- package/test/tls_socket.js +3 -6
- package/tls_socket.js +3 -3
- package/transaction.js +3 -3
- package/address.js +0 -53
- package/endpoint.js +0 -96
- package/host_pool.js +0 -169
- package/outbound/fsync_writestream.js +0 -44
- package/outbound/timer_queue.js +0 -86
- package/rfc1869.js +0 -93
- package/test/endpoint.js +0 -128
- package/test/host_pool.js +0 -188
- package/test/rfc1869.js +0 -89
package/test/server.js
CHANGED
|
@@ -4,14 +4,33 @@ const { describe, it, beforeEach, afterEach } = require('node:test')
|
|
|
4
4
|
const assert = require('node:assert/strict')
|
|
5
5
|
const { createHmac } = require('node:crypto')
|
|
6
6
|
const net = require('node:net')
|
|
7
|
+
const { once } = require('node:events')
|
|
7
8
|
const path = require('node:path')
|
|
8
9
|
const tls = require('node:tls')
|
|
9
10
|
const constants = require('haraka-constants')
|
|
11
|
+
const net_utils = require('haraka-net-utils')
|
|
10
12
|
|
|
11
|
-
const endpoint = require('
|
|
13
|
+
const { endpoint } = require('haraka-net-utils')
|
|
12
14
|
const message = require('haraka-email-message')
|
|
13
15
|
const { get_client } = require('../smtp_client')
|
|
14
16
|
|
|
17
|
+
function fixtureConfig(name) {
|
|
18
|
+
const testRoot = path.resolve('test')
|
|
19
|
+
return require('haraka-config').module_config(testRoot, path.resolve('test/fixtures', name))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function useHaproxyFixture(server, name) {
|
|
23
|
+
const originalConfig = net_utils.config
|
|
24
|
+
const originalConnectionCfg = server.connection.cfg
|
|
25
|
+
const config = fixtureConfig(name)
|
|
26
|
+
net_utils.config = config
|
|
27
|
+
server.connection.cfg = config.get('connection.ini', { booleans: ['+haproxy.enabled'] })
|
|
28
|
+
return () => {
|
|
29
|
+
net_utils.config = originalConfig
|
|
30
|
+
server.connection.cfg = originalConnectionCfg
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
15
34
|
// ─── CRAM-MD5 helper ──────────────────────────────────────────────────────────
|
|
16
35
|
|
|
17
36
|
/** Compute a CRAM-MD5 response to a server challenge. */
|
|
@@ -124,6 +143,35 @@ const sendMessage = ({
|
|
|
124
143
|
)
|
|
125
144
|
})
|
|
126
145
|
|
|
146
|
+
const listen = (server, host = '127.0.0.1') =>
|
|
147
|
+
new Promise((resolve, reject) => {
|
|
148
|
+
server.once('error', reject)
|
|
149
|
+
server.listen(0, host, () => {
|
|
150
|
+
server.removeListener('error', reject)
|
|
151
|
+
resolve()
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
const close = (server) =>
|
|
156
|
+
new Promise((resolve) => {
|
|
157
|
+
server.close(resolve)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
const withTimeout = (promise, ms, msg) =>
|
|
161
|
+
new Promise((resolve, reject) => {
|
|
162
|
+
const timer = setTimeout(() => reject(new Error(msg)), ms)
|
|
163
|
+
promise.then(
|
|
164
|
+
(result) => {
|
|
165
|
+
clearTimeout(timer)
|
|
166
|
+
resolve(result)
|
|
167
|
+
},
|
|
168
|
+
(err) => {
|
|
169
|
+
clearTimeout(timer)
|
|
170
|
+
reject(err)
|
|
171
|
+
},
|
|
172
|
+
)
|
|
173
|
+
})
|
|
174
|
+
|
|
127
175
|
// ─── Tests ────────────────────────────────────────────────────────────────────
|
|
128
176
|
|
|
129
177
|
describe('server', () => {
|
|
@@ -241,6 +289,382 @@ describe('server', () => {
|
|
|
241
289
|
const count = await new Promise((res) => server.getConnections((err, n) => res(n)))
|
|
242
290
|
assert.equal(count, 0)
|
|
243
291
|
})
|
|
292
|
+
|
|
293
|
+
it('accepts PROXY v1 before the SMTPS TLS handshake', async () => {
|
|
294
|
+
const restoreHaproxyConfig = useHaproxyFixture(this.server, 'haproxy_allowed')
|
|
295
|
+
this.server.cfg.main.smtps_port = 0
|
|
296
|
+
|
|
297
|
+
// PROXY-before-TLS takes slightly longer than the default 10 ms timeout on Windows,
|
|
298
|
+
// use 50 ms timeout to avoid flaky tests (default is 300000 ms).
|
|
299
|
+
const server = await this.server.get_smtp_server(endpoint('127.0.0.1:0'), 50)
|
|
300
|
+
const tlsErrors = []
|
|
301
|
+
let raw
|
|
302
|
+
let client
|
|
303
|
+
|
|
304
|
+
server.on('tlsClientError', (err) => {
|
|
305
|
+
tlsErrors.push(err)
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
await listen(server)
|
|
310
|
+
|
|
311
|
+
raw = net.connect(server.address().port, '127.0.0.1')
|
|
312
|
+
await withTimeout(
|
|
313
|
+
Promise.race([
|
|
314
|
+
once(raw, 'connect'),
|
|
315
|
+
once(raw, 'error').then(([err]) => {
|
|
316
|
+
throw err
|
|
317
|
+
}),
|
|
318
|
+
]),
|
|
319
|
+
3000,
|
|
320
|
+
'SMTPS TCP connection timed out',
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
raw.write('PROXY TCP4 127.0.0.1 127.0.0.1 42310 465\r\n')
|
|
324
|
+
client = tls.connect({
|
|
325
|
+
socket: raw,
|
|
326
|
+
rejectUnauthorized: false,
|
|
327
|
+
servername: 'localhost',
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
await withTimeout(
|
|
331
|
+
Promise.race([
|
|
332
|
+
once(client, 'secureConnect'),
|
|
333
|
+
once(client, 'error').then(([err]) => {
|
|
334
|
+
throw err
|
|
335
|
+
}),
|
|
336
|
+
]),
|
|
337
|
+
3000,
|
|
338
|
+
'SMTPS PROXY handshake timed out',
|
|
339
|
+
)
|
|
340
|
+
const [banner] = await withTimeout(once(client, 'data'), 3000, 'SMTPS PROXY banner timed out')
|
|
341
|
+
assert.match(banner.toString(), /^220 /)
|
|
342
|
+
assert.equal(tlsErrors.length, 0)
|
|
343
|
+
} finally {
|
|
344
|
+
if (client) client.destroy()
|
|
345
|
+
else if (raw) raw.destroy()
|
|
346
|
+
await close(server)
|
|
347
|
+
restoreHaproxyConfig()
|
|
348
|
+
}
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
it('accepts direct SMTPS from a PROXY-allowed peer', async () => {
|
|
352
|
+
const restoreHaproxyConfig = useHaproxyFixture(this.server, 'haproxy_allowed')
|
|
353
|
+
this.server.cfg.main.smtps_port = 0
|
|
354
|
+
|
|
355
|
+
const server = await this.server.get_smtp_server(endpoint('127.0.0.1:0'), 1000)
|
|
356
|
+
const tlsErrors = []
|
|
357
|
+
let client
|
|
358
|
+
|
|
359
|
+
server.on('tlsClientError', (err) => {
|
|
360
|
+
tlsErrors.push(err)
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
await listen(server)
|
|
365
|
+
|
|
366
|
+
client = tls.connect({
|
|
367
|
+
port: server.address().port,
|
|
368
|
+
host: '127.0.0.1',
|
|
369
|
+
rejectUnauthorized: false,
|
|
370
|
+
servername: 'localhost',
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
await withTimeout(
|
|
374
|
+
Promise.race([
|
|
375
|
+
once(client, 'secureConnect'),
|
|
376
|
+
once(client, 'error').then(([err]) => {
|
|
377
|
+
throw err
|
|
378
|
+
}),
|
|
379
|
+
]),
|
|
380
|
+
3000,
|
|
381
|
+
'direct SMTPS handshake timed out',
|
|
382
|
+
)
|
|
383
|
+
const [banner] = await withTimeout(once(client, 'data'), 3000, 'direct SMTPS banner timed out')
|
|
384
|
+
assert.match(banner.toString(), /^220 /)
|
|
385
|
+
assert.equal(tlsErrors.length, 0)
|
|
386
|
+
} finally {
|
|
387
|
+
if (client) client.destroy()
|
|
388
|
+
await close(server)
|
|
389
|
+
restoreHaproxyConfig()
|
|
390
|
+
}
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
it('preserves TLS server events for SMTPS connections', async () => {
|
|
394
|
+
this.server.cfg.main.smtps_port = 0
|
|
395
|
+
|
|
396
|
+
const server = await this.server.get_smtp_server(endpoint('127.0.0.1:0'), 10)
|
|
397
|
+
let ocspRequests = 0
|
|
398
|
+
let first
|
|
399
|
+
let second
|
|
400
|
+
|
|
401
|
+
server.tlsServer.on('OCSPRequest', (cert, issuer, cb) => {
|
|
402
|
+
ocspRequests++
|
|
403
|
+
cb()
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
await listen(server)
|
|
408
|
+
|
|
409
|
+
first = tls.connect({
|
|
410
|
+
port: server.address().port,
|
|
411
|
+
host: '127.0.0.1',
|
|
412
|
+
rejectUnauthorized: false,
|
|
413
|
+
requestOCSP: true,
|
|
414
|
+
servername: 'localhost',
|
|
415
|
+
maxVersion: 'TLSv1.2',
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
await withTimeout(
|
|
419
|
+
Promise.race([
|
|
420
|
+
once(first, 'secureConnect'),
|
|
421
|
+
once(first, 'error').then(([err]) => {
|
|
422
|
+
throw err
|
|
423
|
+
}),
|
|
424
|
+
]),
|
|
425
|
+
3000,
|
|
426
|
+
'first SMTPS handshake timed out',
|
|
427
|
+
)
|
|
428
|
+
const session = first.getSession()
|
|
429
|
+
first.destroy()
|
|
430
|
+
await withTimeout(once(first, 'close'), 3000, 'first SMTPS close timed out')
|
|
431
|
+
|
|
432
|
+
second = tls.connect({
|
|
433
|
+
port: server.address().port,
|
|
434
|
+
host: '127.0.0.1',
|
|
435
|
+
rejectUnauthorized: false,
|
|
436
|
+
requestOCSP: true,
|
|
437
|
+
servername: 'localhost',
|
|
438
|
+
maxVersion: 'TLSv1.2',
|
|
439
|
+
session,
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
await withTimeout(
|
|
443
|
+
Promise.race([
|
|
444
|
+
once(second, 'secureConnect'),
|
|
445
|
+
once(second, 'error').then(([err]) => {
|
|
446
|
+
throw err
|
|
447
|
+
}),
|
|
448
|
+
]),
|
|
449
|
+
3000,
|
|
450
|
+
'resumed SMTPS handshake timed out',
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
assert.equal(ocspRequests, 1)
|
|
454
|
+
assert.equal(second.isSessionReused(), true)
|
|
455
|
+
} finally {
|
|
456
|
+
if (second) second.destroy()
|
|
457
|
+
if (first) first.destroy()
|
|
458
|
+
await close(server)
|
|
459
|
+
}
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
it('uses direct TLS for SMTPS when HAProxy support is disabled', async () => {
|
|
463
|
+
const restoreHaproxyConfig = useHaproxyFixture(this.server, 'haproxy_disabled')
|
|
464
|
+
this.server.cfg.main.smtps_port = 0
|
|
465
|
+
|
|
466
|
+
let server
|
|
467
|
+
let client
|
|
468
|
+
|
|
469
|
+
try {
|
|
470
|
+
server = await this.server.get_smtp_server(endpoint('127.0.0.1:0'), 10)
|
|
471
|
+
assert.equal(server.tlsServer, undefined)
|
|
472
|
+
|
|
473
|
+
await listen(server)
|
|
474
|
+
|
|
475
|
+
client = tls.connect({
|
|
476
|
+
port: server.address().port,
|
|
477
|
+
host: '127.0.0.1',
|
|
478
|
+
rejectUnauthorized: false,
|
|
479
|
+
servername: 'localhost',
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
await withTimeout(
|
|
483
|
+
Promise.race([
|
|
484
|
+
once(client, 'secureConnect'),
|
|
485
|
+
once(client, 'error').then(([err]) => {
|
|
486
|
+
throw err
|
|
487
|
+
}),
|
|
488
|
+
]),
|
|
489
|
+
3000,
|
|
490
|
+
'direct TLS fallback handshake timed out',
|
|
491
|
+
)
|
|
492
|
+
} finally {
|
|
493
|
+
if (client) client.destroy()
|
|
494
|
+
if (server) await close(server)
|
|
495
|
+
restoreHaproxyConfig()
|
|
496
|
+
}
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
it('accepts direct SMTPS from an untrusted PROXY peer', async () => {
|
|
500
|
+
const restoreHaproxyConfig = useHaproxyFixture(this.server, 'haproxy_untrusted')
|
|
501
|
+
this.server.cfg.main.smtps_port = 0
|
|
502
|
+
|
|
503
|
+
const server = await this.server.get_smtp_server(endpoint('127.0.0.1:0'), 10)
|
|
504
|
+
let client
|
|
505
|
+
|
|
506
|
+
try {
|
|
507
|
+
await listen(server)
|
|
508
|
+
|
|
509
|
+
client = tls.connect({
|
|
510
|
+
port: server.address().port,
|
|
511
|
+
host: '127.0.0.1',
|
|
512
|
+
rejectUnauthorized: false,
|
|
513
|
+
servername: 'localhost',
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
await withTimeout(
|
|
517
|
+
Promise.race([
|
|
518
|
+
once(client, 'secureConnect'),
|
|
519
|
+
once(client, 'error').then(([err]) => {
|
|
520
|
+
throw err
|
|
521
|
+
}),
|
|
522
|
+
]),
|
|
523
|
+
3000,
|
|
524
|
+
'untrusted direct SMTPS handshake timed out',
|
|
525
|
+
)
|
|
526
|
+
} finally {
|
|
527
|
+
if (client) client.destroy()
|
|
528
|
+
await close(server)
|
|
529
|
+
restoreHaproxyConfig()
|
|
530
|
+
}
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
it('rejects malformed SMTPS PROXY lines before TLS', async () => {
|
|
534
|
+
const restoreHaproxyConfig = useHaproxyFixture(this.server, 'haproxy_allowed')
|
|
535
|
+
this.server.cfg.main.smtps_port = 0
|
|
536
|
+
|
|
537
|
+
const server = await this.server.get_smtp_server(endpoint('127.0.0.1:0'), 10)
|
|
538
|
+
let raw
|
|
539
|
+
|
|
540
|
+
try {
|
|
541
|
+
await listen(server)
|
|
542
|
+
|
|
543
|
+
raw = net.connect(server.address().port, '127.0.0.1')
|
|
544
|
+
await withTimeout(once(raw, 'connect'), 3000, 'malformed PROXY TCP connection timed out')
|
|
545
|
+
raw.write('PROXY TCP4 nope 127.0.0.1 42310 465\r\n')
|
|
546
|
+
|
|
547
|
+
const [response] = await withTimeout(once(raw, 'data'), 3000, 'malformed PROXY response timed out')
|
|
548
|
+
assert.match(response.toString(), /^421 Invalid PROXY format/)
|
|
549
|
+
} finally {
|
|
550
|
+
if (raw) raw.destroy()
|
|
551
|
+
await close(server)
|
|
552
|
+
restoreHaproxyConfig()
|
|
553
|
+
}
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
it('rejects oversized SMTPS PROXY lines before TLS', async () => {
|
|
557
|
+
const restoreHaproxyConfig = useHaproxyFixture(this.server, 'haproxy_allowed')
|
|
558
|
+
this.server.cfg.main.smtps_port = 0
|
|
559
|
+
|
|
560
|
+
const server = await this.server.get_smtp_server(endpoint('127.0.0.1:0'), 10)
|
|
561
|
+
let raw
|
|
562
|
+
|
|
563
|
+
try {
|
|
564
|
+
await listen(server)
|
|
565
|
+
|
|
566
|
+
raw = net.connect(server.address().port, '127.0.0.1')
|
|
567
|
+
await withTimeout(once(raw, 'connect'), 3000, 'oversized PROXY TCP connection timed out')
|
|
568
|
+
raw.write(`PROXY ${'x'.repeat(513)}`)
|
|
569
|
+
|
|
570
|
+
const [response] = await withTimeout(once(raw, 'data'), 3000, 'oversized PROXY response timed out')
|
|
571
|
+
assert.match(response.toString(), /^421 Invalid PROXY format/)
|
|
572
|
+
} finally {
|
|
573
|
+
if (raw) raw.destroy()
|
|
574
|
+
await close(server)
|
|
575
|
+
restoreHaproxyConfig()
|
|
576
|
+
}
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
it('times out waiting for SMTPS PROXY from an allowed peer', async () => {
|
|
580
|
+
const restoreHaproxyConfig = useHaproxyFixture(this.server, 'haproxy_allowed')
|
|
581
|
+
const originalSetTimeout = global.setTimeout
|
|
582
|
+
global.setTimeout = (fn, ms, ...args) => originalSetTimeout(fn, ms === 30 * 1000 ? 20 : ms, ...args)
|
|
583
|
+
this.server.cfg.main.smtps_port = 0
|
|
584
|
+
|
|
585
|
+
const server = await this.server.get_smtp_server(endpoint('127.0.0.1:0'), 10)
|
|
586
|
+
let raw
|
|
587
|
+
|
|
588
|
+
try {
|
|
589
|
+
await listen(server)
|
|
590
|
+
|
|
591
|
+
raw = net.connect(server.address().port, '127.0.0.1')
|
|
592
|
+
await withTimeout(once(raw, 'connect'), 3000, 'PROXY timeout TCP connection timed out')
|
|
593
|
+
|
|
594
|
+
const [response] = await withTimeout(once(raw, 'data'), 3000, 'PROXY timeout response timed out')
|
|
595
|
+
assert.match(response.toString(), /^421 PROXY timeout/)
|
|
596
|
+
} finally {
|
|
597
|
+
global.setTimeout = originalSetTimeout
|
|
598
|
+
if (raw) raw.destroy()
|
|
599
|
+
await close(server)
|
|
600
|
+
restoreHaproxyConfig()
|
|
601
|
+
}
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
it('accepts byte-by-byte direct SMTPS from a PROXY-allowed peer', async () => {
|
|
605
|
+
const restoreHaproxyConfig = useHaproxyFixture(this.server, 'haproxy_allowed')
|
|
606
|
+
this.server.cfg.main.smtps_port = 0
|
|
607
|
+
|
|
608
|
+
const server = await this.server.get_smtp_server(endpoint('127.0.0.1:0'), 1000)
|
|
609
|
+
let raw
|
|
610
|
+
let client
|
|
611
|
+
|
|
612
|
+
try {
|
|
613
|
+
await listen(server)
|
|
614
|
+
|
|
615
|
+
raw = net.connect(server.address().port, '127.0.0.1')
|
|
616
|
+
await withTimeout(once(raw, 'connect'), 3000, 'fragmented direct SMTPS TCP connection timed out')
|
|
617
|
+
|
|
618
|
+
const write = raw.write.bind(raw)
|
|
619
|
+
raw.write = (chunk, encoding, cb) => {
|
|
620
|
+
if (typeof encoding === 'function') {
|
|
621
|
+
cb = encoding
|
|
622
|
+
encoding = undefined
|
|
623
|
+
}
|
|
624
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding)
|
|
625
|
+
let pos = 0
|
|
626
|
+
const write_next = () => {
|
|
627
|
+
if (pos >= buffer.length) {
|
|
628
|
+
if (cb) cb()
|
|
629
|
+
return
|
|
630
|
+
}
|
|
631
|
+
write(buffer.subarray(pos, pos + 1))
|
|
632
|
+
pos++
|
|
633
|
+
setImmediate(write_next)
|
|
634
|
+
}
|
|
635
|
+
write_next()
|
|
636
|
+
return true
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
client = tls.connect({
|
|
640
|
+
socket: raw,
|
|
641
|
+
rejectUnauthorized: false,
|
|
642
|
+
servername: 'localhost',
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
await withTimeout(
|
|
646
|
+
Promise.race([
|
|
647
|
+
once(client, 'secureConnect'),
|
|
648
|
+
once(client, 'error').then(([err]) => {
|
|
649
|
+
throw err
|
|
650
|
+
}),
|
|
651
|
+
]),
|
|
652
|
+
3000,
|
|
653
|
+
'fragmented direct SMTPS handshake timed out',
|
|
654
|
+
)
|
|
655
|
+
const [banner] = await withTimeout(
|
|
656
|
+
once(client, 'data'),
|
|
657
|
+
3000,
|
|
658
|
+
'fragmented direct SMTPS banner timed out',
|
|
659
|
+
)
|
|
660
|
+
assert.match(banner.toString(), /^220 /)
|
|
661
|
+
} finally {
|
|
662
|
+
if (client) client.destroy()
|
|
663
|
+
else if (raw) raw.destroy()
|
|
664
|
+
await close(server)
|
|
665
|
+
restoreHaproxyConfig()
|
|
666
|
+
}
|
|
667
|
+
})
|
|
244
668
|
})
|
|
245
669
|
|
|
246
670
|
// ── get_http_docroot ──────────────────────────────────────────────────────
|
package/test/smtp_client.js
CHANGED
|
@@ -5,7 +5,7 @@ const assert = require('node:assert/strict')
|
|
|
5
5
|
const { PassThrough } = require('node:stream')
|
|
6
6
|
const path = require('node:path')
|
|
7
7
|
|
|
8
|
-
const { Address } = require('
|
|
8
|
+
const { Address } = require('@haraka/email-address')
|
|
9
9
|
const fixtures = require('haraka-test-fixtures')
|
|
10
10
|
const net_utils = require('haraka-net-utils')
|
|
11
11
|
const message = require('haraka-email-message')
|
|
@@ -57,19 +57,18 @@ function restoreTlsConnect() {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
function makeConnection(overrides = {}) {
|
|
60
|
-
const conn = fixtures.
|
|
60
|
+
const conn = fixtures.makeConnection({ ip: '1.2.3.4' })
|
|
61
61
|
conn.server = { notes: {} }
|
|
62
62
|
conn.hello = { host: 'client.example.com' }
|
|
63
63
|
conn.local = { host: 'relay.example.com' }
|
|
64
|
-
conn.remote = { ip: '1.2.3.4' }
|
|
65
64
|
conn.transaction = null
|
|
66
65
|
return Object.assign(conn, overrides)
|
|
67
66
|
}
|
|
68
67
|
|
|
69
68
|
function makePlugin() {
|
|
70
|
-
const p =
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
const p = fixtures.makePlugin('queue/smtp_forward', {
|
|
70
|
+
configDir: path.resolve('test'),
|
|
71
|
+
})
|
|
73
72
|
p.tls_options = {}
|
|
74
73
|
return p
|
|
75
74
|
}
|
|
@@ -228,7 +227,7 @@ describe('SMTPClient line handler', () => {
|
|
|
228
227
|
client.command = 'starttls'
|
|
229
228
|
client.tls_options = { servername: 'mx.example.com' }
|
|
230
229
|
let upgradeCalled = false
|
|
231
|
-
client.socket.upgrade = (
|
|
230
|
+
client.socket.upgrade = () => {
|
|
232
231
|
upgradeCalled = true
|
|
233
232
|
}
|
|
234
233
|
client.socket.emit('line', '220 Go ahead\r\n')
|
|
@@ -249,10 +248,7 @@ describe('SMTPClient line handler', () => {
|
|
|
249
248
|
it('returns early after bad_code when state is not ACTIVE', () => {
|
|
250
249
|
client.command = 'mail'
|
|
251
250
|
client.state = STATE.IDLE
|
|
252
|
-
|
|
253
|
-
client.on('helo', () => {
|
|
254
|
-
heloFired = true
|
|
255
|
-
}) // shouldn't fire
|
|
251
|
+
client.on('helo', () => {}) // shouldn't fire
|
|
256
252
|
let badCodeFired = false
|
|
257
253
|
client.on('bad_code', () => {
|
|
258
254
|
badCodeFired = true
|
|
@@ -344,7 +340,7 @@ describe('SMTPClient socket connect event', () => {
|
|
|
344
340
|
socket.setTimeout = (ms) => {
|
|
345
341
|
lastTimeout = ms
|
|
346
342
|
}
|
|
347
|
-
|
|
343
|
+
makeClient({ socket, idle_timeout: 120 })
|
|
348
344
|
socket.emit('connect')
|
|
349
345
|
assert.equal(lastTimeout, 120_000)
|
|
350
346
|
})
|
|
@@ -513,7 +509,7 @@ describe('SMTPClient#start_data', () => {
|
|
|
513
509
|
const client = makeClient()
|
|
514
510
|
let pipeTarget = null
|
|
515
511
|
const mockStream = {
|
|
516
|
-
pipe: (dest
|
|
512
|
+
pipe: (dest) => {
|
|
517
513
|
pipeTarget = dest
|
|
518
514
|
},
|
|
519
515
|
}
|
|
@@ -591,7 +587,7 @@ describe('SMTPClient#upgrade', () => {
|
|
|
591
587
|
it('delegates to socket.upgrade with tls_options', () => {
|
|
592
588
|
const socket = makeSocket()
|
|
593
589
|
let upgradeOpts = null
|
|
594
|
-
socket.upgrade = (opts
|
|
590
|
+
socket.upgrade = (opts) => {
|
|
595
591
|
upgradeOpts = opts
|
|
596
592
|
}
|
|
597
593
|
const client = makeClient({ socket })
|
|
@@ -964,7 +960,7 @@ describe('smtp_client.get_client_plugin', () => {
|
|
|
964
960
|
})
|
|
965
961
|
|
|
966
962
|
it('reuses existing host_pool from server.notes', async () => {
|
|
967
|
-
const HostPool = require('
|
|
963
|
+
const { HostPool } = require('haraka-net-utils')
|
|
968
964
|
const pool = new HostPool('10.0.0.3:25')
|
|
969
965
|
conn.server.notes.host_pool = pool
|
|
970
966
|
await getClientPlugin({ forwarding_host_pool: '10.0.0.3:25' })
|
|
@@ -1021,7 +1017,6 @@ describe('smtp_client.get_client_plugin', () => {
|
|
|
1021
1017
|
return s
|
|
1022
1018
|
}
|
|
1023
1019
|
|
|
1024
|
-
const written = []
|
|
1025
1020
|
const mockPlugin = makePlugin()
|
|
1026
1021
|
|
|
1027
1022
|
smtp_client_module.get_client_plugin(
|
|
@@ -1257,8 +1252,6 @@ describe('smtp_client full session (auth)', () => {
|
|
|
1257
1252
|
|
|
1258
1253
|
describe('smtp_client', () => {
|
|
1259
1254
|
it('testUpgradeIsCalledOnSTARTTLS', () => {
|
|
1260
|
-
const plugin = makePlugin()
|
|
1261
|
-
|
|
1262
1255
|
const cmds = {}
|
|
1263
1256
|
let upgradeArgs = {}
|
|
1264
1257
|
|
package/test/tls_socket.js
CHANGED
|
@@ -8,9 +8,6 @@ const tls = require('node:tls')
|
|
|
8
8
|
const fs = require('node:fs')
|
|
9
9
|
const { EventEmitter } = require('node:events')
|
|
10
10
|
|
|
11
|
-
// Mock dependencies before requiring the target
|
|
12
|
-
const mock = require('node:test').mock
|
|
13
|
-
|
|
14
11
|
const tls_socket = require('../tls_socket')
|
|
15
12
|
|
|
16
13
|
const TEST_CERT = fs.readFileSync(path.join(__dirname, 'config/tls_cert.pem'))
|
|
@@ -55,12 +52,12 @@ test('tls_socket', async (t) => {
|
|
|
55
52
|
})
|
|
56
53
|
})
|
|
57
54
|
|
|
58
|
-
await t.test('pluggableStream', async (
|
|
55
|
+
await t.test('pluggableStream', async () => {
|
|
59
56
|
// This is a class inside the file, but not exported.
|
|
60
57
|
// We can test it via createServer or connect if we mock net.
|
|
61
58
|
})
|
|
62
59
|
|
|
63
|
-
await t.test('connect', async (
|
|
60
|
+
await t.test('connect', async () => {
|
|
64
61
|
// Exercise the `new tls.connect` bug
|
|
65
62
|
// We can't easily catch the 'new' keyword usage without proxying tls.connect
|
|
66
63
|
assert.strictEqual(typeof tls_socket.connect, 'function')
|
|
@@ -141,7 +138,7 @@ test('tls_socket', async (t) => {
|
|
|
141
138
|
})
|
|
142
139
|
})
|
|
143
140
|
|
|
144
|
-
await t.test('getSocketOpts', async (
|
|
141
|
+
await t.test('getSocketOpts', async () => {
|
|
145
142
|
// Exercise the typo path (would requires failing config.getDir)
|
|
146
143
|
assert.strictEqual(typeof tls_socket.getSocketOpts, 'function')
|
|
147
144
|
})
|
package/tls_socket.js
CHANGED
|
@@ -95,7 +95,7 @@ class pluggableStream extends stream.Stream {
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
clean(
|
|
98
|
+
clean() {
|
|
99
99
|
if (this.targetsocket?.removeAllListeners) {
|
|
100
100
|
for (const name of ['data', 'secure', 'secureConnect', 'end', 'close', 'error', 'drain']) {
|
|
101
101
|
this.targetsocket.removeAllListeners(name)
|
|
@@ -540,7 +540,7 @@ exports.getSocketOpts = async (name) => {
|
|
|
540
540
|
function pipe(cleartext, socket) {
|
|
541
541
|
cleartext.socket = socket
|
|
542
542
|
|
|
543
|
-
function onError(
|
|
543
|
+
function onError() {}
|
|
544
544
|
|
|
545
545
|
function onClose() {
|
|
546
546
|
socket.removeListener('error', onError)
|
|
@@ -570,7 +570,7 @@ exports.ensureDhparams = (done) => {
|
|
|
570
570
|
log.debug(data)
|
|
571
571
|
})
|
|
572
572
|
|
|
573
|
-
o.stderr.on('data', (
|
|
573
|
+
o.stderr.on('data', () => {
|
|
574
574
|
// this is the status gibberish `openssl dhparam` spews as it works
|
|
575
575
|
})
|
|
576
576
|
|
package/transaction.js
CHANGED
|
@@ -45,7 +45,7 @@ class Transaction {
|
|
|
45
45
|
if (this.body) return
|
|
46
46
|
|
|
47
47
|
this.body = new message.Body(this.header)
|
|
48
|
-
this.body.on('mime_boundary', (
|
|
48
|
+
this.body.on('mime_boundary', () => this.incr_mime_count())
|
|
49
49
|
|
|
50
50
|
for (const hook of this.attachment_start_hooks) {
|
|
51
51
|
this.body.on('attachment_start', hook)
|
|
@@ -226,7 +226,7 @@ class Transaction {
|
|
|
226
226
|
if (this.found_hb_sep) this.reset_headers()
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
attachment_hooks(start
|
|
229
|
+
attachment_hooks(start) {
|
|
230
230
|
this.parse_body = true
|
|
231
231
|
this.attachment_start_hooks.push(start)
|
|
232
232
|
}
|
|
@@ -255,7 +255,7 @@ class Transaction {
|
|
|
255
255
|
}
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
incr_mime_count(
|
|
258
|
+
incr_mime_count() {
|
|
259
259
|
this.mime_part_count++
|
|
260
260
|
}
|
|
261
261
|
}
|