Haraka 3.2.0 → 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 -2
- 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 +51 -48
- 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/host_pool.js
DELETED
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const { describe, it } = require('node:test')
|
|
4
|
-
const assert = require('node:assert/strict')
|
|
5
|
-
|
|
6
|
-
const HostPool = require('../host_pool')
|
|
7
|
-
|
|
8
|
-
describe('HostPool', () => {
|
|
9
|
-
it('get a host', () => {
|
|
10
|
-
const pool = new HostPool('1.1.1.1:1111, 2.2.2.2:2222')
|
|
11
|
-
const host = pool.get_host()
|
|
12
|
-
|
|
13
|
-
assert.ok(/\d\.\d\.\d\.\d/.test(host.host), `'${host.host}' looks like a IP`)
|
|
14
|
-
assert.ok(/\d\d\d\d/.test(host.port), `'${host.port}' looks like a port`)
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
it('uses all the list', () => {
|
|
18
|
-
const pool = new HostPool('1.1.1.1:1111, 2.2.2.2:2222')
|
|
19
|
-
|
|
20
|
-
const host1 = pool.get_host()
|
|
21
|
-
const host2 = pool.get_host()
|
|
22
|
-
const host3 = pool.get_host()
|
|
23
|
-
|
|
24
|
-
assert.notEqual(host1.host, host2.host)
|
|
25
|
-
assert.notEqual(host3.host, host2.host)
|
|
26
|
-
assert.equal(host3.host, host1.host)
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
it('default port 25', () => {
|
|
30
|
-
const pool = new HostPool('1.1.1.1, 2.2.2.2')
|
|
31
|
-
|
|
32
|
-
const host1 = pool.get_host()
|
|
33
|
-
const host2 = pool.get_host()
|
|
34
|
-
|
|
35
|
-
assert.equal(host1.port, 25, `is port 25: ${host1.port}`)
|
|
36
|
-
assert.equal(host2.port, 25, `is port 25: ${host2.port}`)
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
it('dead host', () => {
|
|
40
|
-
const pool = new HostPool('1.1.1.1:1111, 2.2.2.2:2222', 0.001)
|
|
41
|
-
pool.get_socket = () => ({
|
|
42
|
-
pretendTimeout: () => {},
|
|
43
|
-
setTimeout(ms, cb) {
|
|
44
|
-
this.pretendTimeout = cb
|
|
45
|
-
},
|
|
46
|
-
listeners: {},
|
|
47
|
-
on(ev, cb) {
|
|
48
|
-
this.listeners[ev] = cb
|
|
49
|
-
},
|
|
50
|
-
connect(port, host, cb) {
|
|
51
|
-
cb()
|
|
52
|
-
}, // immediately "connects" to stop retry loop
|
|
53
|
-
destroy() {},
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
pool.failed('1.1.1.1', '1111')
|
|
57
|
-
|
|
58
|
-
let host
|
|
59
|
-
|
|
60
|
-
host = pool.get_host()
|
|
61
|
-
assert.equal(host.host, '2.2.2.2', 'dead host is not returned')
|
|
62
|
-
host = pool.get_host()
|
|
63
|
-
assert.equal(host.host, '2.2.2.2', 'dead host is not returned')
|
|
64
|
-
host = pool.get_host()
|
|
65
|
-
assert.equal(host.host, '2.2.2.2', 'dead host is not returned')
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
// if they're *all* dead, we return a host to try anyway, to keep from
|
|
69
|
-
// accidentally DOS'ing ourselves if there's a transient but widespread
|
|
70
|
-
// network outage
|
|
71
|
-
it("they're all dead", () => {
|
|
72
|
-
let host1
|
|
73
|
-
let host2
|
|
74
|
-
|
|
75
|
-
const pool = new HostPool('1.1.1.1:1111, 2.2.2.2:2222', 0.001)
|
|
76
|
-
pool.get_socket = () => ({
|
|
77
|
-
pretendTimeout: () => {},
|
|
78
|
-
setTimeout(ms, cb) {
|
|
79
|
-
this.pretendTimeout = cb
|
|
80
|
-
},
|
|
81
|
-
listeners: {},
|
|
82
|
-
on(ev, cb) {
|
|
83
|
-
this.listeners[ev] = cb
|
|
84
|
-
},
|
|
85
|
-
connect(port, host, cb) {
|
|
86
|
-
cb()
|
|
87
|
-
}, // immediately "connects" to stop retry loop
|
|
88
|
-
destroy() {},
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
host1 = pool.get_host()
|
|
92
|
-
|
|
93
|
-
pool.failed('1.1.1.1', '1111')
|
|
94
|
-
pool.failed('2.2.2.2', '2222')
|
|
95
|
-
|
|
96
|
-
host2 = pool.get_host()
|
|
97
|
-
assert.ok(host2, "if they're all dead, try one anyway")
|
|
98
|
-
assert.notEqual(host1.host, host2.host, 'rotation continues')
|
|
99
|
-
|
|
100
|
-
host1 = pool.get_host()
|
|
101
|
-
assert.ok(host1, "if they're all dead, try one anyway")
|
|
102
|
-
assert.notEqual(host1.host, host2.host, 'rotation continues')
|
|
103
|
-
|
|
104
|
-
host2 = pool.get_host()
|
|
105
|
-
assert.ok(host2, "if they're all dead, try one anyway")
|
|
106
|
-
assert.notEqual(host1.host, host2.host, 'rotation continues')
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
// after .01 secs the timer to retry the dead host will fire, and then
|
|
110
|
-
// we connect using this mock socket, whose "connect" always succeeds
|
|
111
|
-
// so the code brings the dead host back to life
|
|
112
|
-
it('host dead checking timer', async () => {
|
|
113
|
-
let num_reqs = 0
|
|
114
|
-
const MockSocket = function MockSocket(pool) {
|
|
115
|
-
// these are the methods called from probe_dead_host
|
|
116
|
-
|
|
117
|
-
// setTimeout on the socket
|
|
118
|
-
this.pretendTimeout = () => {}
|
|
119
|
-
this.setTimeout = (ms, cb) => {
|
|
120
|
-
this.pretendTimeout = cb
|
|
121
|
-
}
|
|
122
|
-
// handle socket.on('error', ....
|
|
123
|
-
this.listeners = {}
|
|
124
|
-
this.on = (eventname, cb) => {
|
|
125
|
-
this.listeners[eventname] = cb
|
|
126
|
-
}
|
|
127
|
-
this.emit = (eventname) => {
|
|
128
|
-
this.listeners[eventname]()
|
|
129
|
-
}
|
|
130
|
-
// handle socket.connect(...
|
|
131
|
-
this.connected = () => {}
|
|
132
|
-
this.connect = (port, host, cb) => {
|
|
133
|
-
switch (++num_reqs) {
|
|
134
|
-
case 1:
|
|
135
|
-
// the first time through we pretend it timed out
|
|
136
|
-
this.pretendTimeout()
|
|
137
|
-
break
|
|
138
|
-
case 2:
|
|
139
|
-
// the second time through, pretend socket error
|
|
140
|
-
this.emit('error')
|
|
141
|
-
break
|
|
142
|
-
case 3:
|
|
143
|
-
// the third time around, the socket connected
|
|
144
|
-
cb()
|
|
145
|
-
break
|
|
146
|
-
default:
|
|
147
|
-
// failsafe
|
|
148
|
-
console.log(`num_reqs hit ${num_reqs}, wtf?`)
|
|
149
|
-
process.exit(1)
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
this.destroy = () => {}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const retry_secs = 0.001 // 1ms
|
|
156
|
-
const pool = new HostPool('1.1.1.1:1111, 2.2.2.2:2222', retry_secs)
|
|
157
|
-
|
|
158
|
-
// override the pool's get_socket method to return our mock
|
|
159
|
-
pool.get_socket = () => new MockSocket(pool)
|
|
160
|
-
|
|
161
|
-
// mark the host as failed and start up the retry timers
|
|
162
|
-
pool.failed('1.1.1.1', '1111')
|
|
163
|
-
|
|
164
|
-
assert.ok(pool.dead_hosts['1.1.1.1:1111'], 'yes it was marked dead')
|
|
165
|
-
|
|
166
|
-
// probe_dead_host() will hit two failures and one success (based on
|
|
167
|
-
// num_reqs above). So we wait at least 10s for that to happen:
|
|
168
|
-
await new Promise((resolve, reject) => {
|
|
169
|
-
const timer = setTimeout(() => {
|
|
170
|
-
clearInterval(interval)
|
|
171
|
-
reject(new Error('probe_dead_host failed'))
|
|
172
|
-
}, 10 * 1000)
|
|
173
|
-
|
|
174
|
-
const interval = setInterval(
|
|
175
|
-
() => {
|
|
176
|
-
if (!pool.dead_hosts['1.1.1.1:1111']) {
|
|
177
|
-
clearTimeout(timer)
|
|
178
|
-
clearInterval(interval)
|
|
179
|
-
resolve()
|
|
180
|
-
}
|
|
181
|
-
},
|
|
182
|
-
retry_secs * 1000 * 3,
|
|
183
|
-
)
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
assert.ok(true, 'timer un-deaded it')
|
|
187
|
-
})
|
|
188
|
-
})
|
package/test/rfc1869.js
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const { describe, it } = require('node:test')
|
|
4
|
-
const assert = require('node:assert/strict')
|
|
5
|
-
|
|
6
|
-
const { parse } = require('../rfc1869')
|
|
7
|
-
|
|
8
|
-
function _check(line, expected) {
|
|
9
|
-
const match = /^(MAIL|RCPT)\s+(.*)$/.exec(line)
|
|
10
|
-
const parsed = parse(match[1].toLowerCase(), match[2])
|
|
11
|
-
assert.deepEqual(parsed, expected)
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
describe('rfc1869', () => {
|
|
15
|
-
describe('valid parse cases', () => {
|
|
16
|
-
const validCases = [
|
|
17
|
-
// MAIL FROM variants
|
|
18
|
-
['MAIL FROM:<>', ['<>']],
|
|
19
|
-
['MAIL FROM:', ['<>']],
|
|
20
|
-
['MAIL FROM:<postmaster>', ['<postmaster>']],
|
|
21
|
-
['MAIL FROM:user', ['user']],
|
|
22
|
-
['MAIL FROM:user size=1234', ['user', 'size=1234']],
|
|
23
|
-
['MAIL FROM:user@domain size=1234', ['user@domain', 'size=1234']],
|
|
24
|
-
['MAIL FROM:<user@domain> size=1234', ['<user@domain>', 'size=1234']],
|
|
25
|
-
['MAIL FROM:<user@domain> somekey', ['<user@domain>', 'somekey']],
|
|
26
|
-
['MAIL FROM:<user@domain> somekey other=foo', ['<user@domain>', 'somekey', 'other=foo']],
|
|
27
|
-
// RFC 1652 BODY extension keyword
|
|
28
|
-
['MAIL FROM:<user@domain> BODY=8BITMIME', ['<user@domain>', 'BODY=8BITMIME']],
|
|
29
|
-
// RFC 6531 SMTPUTF8 keyword (no value)
|
|
30
|
-
['MAIL FROM:<user@domain> SMTPUTF8', ['<user@domain>', 'SMTPUTF8']],
|
|
31
|
-
// RCPT TO variants
|
|
32
|
-
['RCPT TO: 0@mailblog.biz 0=9 1=9', ['<0@mailblog.biz>', '0=9', '1=9']],
|
|
33
|
-
['RCPT TO:<r86x-ray@emailitin.com> state=1', ['<r86x-ray@emailitin.com>', 'state=1']],
|
|
34
|
-
['RCPT TO:<user=name@domain.com> foo=bar', ['<user=name@domain.com>', 'foo=bar']],
|
|
35
|
-
['RCPT TO:<postmaster>', ['<postmaster>']],
|
|
36
|
-
['RCPT TO:<abuse>', ['<abuse>']],
|
|
37
|
-
]
|
|
38
|
-
|
|
39
|
-
for (const [line, expected] of validCases) {
|
|
40
|
-
it(line, () => _check(line, expected))
|
|
41
|
-
}
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
describe('error cases', () => {
|
|
45
|
-
const throwCases = [
|
|
46
|
-
{
|
|
47
|
-
desc: 'MAIL FROM with space inside angle-bracket address',
|
|
48
|
-
args: ['mail', 'FROM:<user@dom ain>'],
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
desc: 'RCPT TO with syntax error in address (space in address)',
|
|
52
|
-
args: ['rcpt', 'TO: user @domain bad'],
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
desc: 'RCPT TO unknown address (no @ and not postmaster/abuse)',
|
|
56
|
-
args: ['rcpt', 'TO:unknown'],
|
|
57
|
-
},
|
|
58
|
-
]
|
|
59
|
-
|
|
60
|
-
for (const { desc, args } of throwCases) {
|
|
61
|
-
it(`throws: ${desc}`, () => {
|
|
62
|
-
assert.throws(() => parse(...args), Error)
|
|
63
|
-
})
|
|
64
|
-
}
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
describe('strict mode', () => {
|
|
68
|
-
const strictValidCases = [
|
|
69
|
-
['mail', 'FROM:<user@domain.com>', '<user@domain.com>'],
|
|
70
|
-
['rcpt', 'TO:<user@domain.com>', '<user@domain.com>'],
|
|
71
|
-
]
|
|
72
|
-
for (const [type, line, expected] of strictValidCases) {
|
|
73
|
-
it(`strict ${type.toUpperCase()} with angle brackets accepts address`, () => {
|
|
74
|
-
const result = parse(type, line, true)
|
|
75
|
-
assert.equal(result[0], expected)
|
|
76
|
-
})
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const strictThrowCases = [
|
|
80
|
-
['mail', 'FROM:user@domain.com'],
|
|
81
|
-
['rcpt', 'TO:user@domain.com'],
|
|
82
|
-
]
|
|
83
|
-
for (const [type, line] of strictThrowCases) {
|
|
84
|
-
it(`strict ${type.toUpperCase()} without angle brackets throws`, () => {
|
|
85
|
-
assert.throws(() => parse(type, line, true), Error)
|
|
86
|
-
})
|
|
87
|
-
}
|
|
88
|
-
})
|
|
89
|
-
})
|