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/address.js
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
// SUNSET 2027: Haraka core constructs envelope addresses with
|
|
4
|
-
// @haraka/email-address, whose `.address` / `.host` are string
|
|
5
|
-
// properties. Core itself — and a large body of bundled and
|
|
6
|
-
// third-party plugins — still use the historical address-rfc2821 /
|
|
7
|
-
// address-rfc2822 *method* contract (`addr.address()`, `addr.host()`).
|
|
8
|
-
//
|
|
9
|
-
// `asLegacy()` wraps each instance so both the new string-property API
|
|
10
|
-
// and the legacy callable form work during the transition. The wrap is
|
|
11
|
-
// idempotent, and `unwrapLegacy()` recovers the raw instance so the
|
|
12
|
-
// outbound queue's JSON re-hydration copies primitive string fields
|
|
13
|
-
// rather than the callable accessors.
|
|
14
|
-
//
|
|
15
|
-
// Once the ecosystem has migrated, delete this module, require
|
|
16
|
-
// `Address` straight from '@haraka/email-address', and drop the wrapper
|
|
17
|
-
// (see @haraka/email-address lib/legacy.js).
|
|
18
|
-
|
|
19
|
-
const { Address: BaseAddress, asLegacy, unwrapLegacy } = require('@haraka/email-address')
|
|
20
|
-
|
|
21
|
-
class Address extends BaseAddress {
|
|
22
|
-
constructor(...args) {
|
|
23
|
-
// never re-hydrate from a wrapped instance — copy raw strings
|
|
24
|
-
if (args.length) args[0] = unwrapLegacy(args[0])
|
|
25
|
-
// `new Address(user, host)` where `host` is another wrapped
|
|
26
|
-
// address's `.host` — the SUNSET-2027 callable accessor is
|
|
27
|
-
// `typeof 'function'`, which BaseAddress would mistake for an
|
|
28
|
-
// options object. Coerce it back to the primitive string.
|
|
29
|
-
if (args.length >= 2 && typeof args[1] === 'function') {
|
|
30
|
-
args[1] = String(args[1])
|
|
31
|
-
}
|
|
32
|
-
super(...args)
|
|
33
|
-
return asLegacy(this)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Preserve the address-rfc2821 wire shape so existing on-disk queue
|
|
37
|
-
// files stay byte-compatible across the upgrade and re-hydrate
|
|
38
|
-
// unchanged. @haraka/email-address additionally carries
|
|
39
|
-
// phrase/comment/group/opts, which are irrelevant to envelope
|
|
40
|
-
// addresses and must not leak into the persisted todo. SUNSET 2027.
|
|
41
|
-
toJSON() {
|
|
42
|
-
const out = {
|
|
43
|
-
original: this.original,
|
|
44
|
-
original_host: this.original_host,
|
|
45
|
-
host: this.host,
|
|
46
|
-
user: this.user,
|
|
47
|
-
}
|
|
48
|
-
if (this.is_utf8) out.is_utf8 = true
|
|
49
|
-
return out
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
module.exports = { Address }
|
package/endpoint.js
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
// Socket address parser/formatter and server binding helper
|
|
3
|
-
|
|
4
|
-
const fs = require('node:fs/promises')
|
|
5
|
-
const net = require('node:net')
|
|
6
|
-
|
|
7
|
-
function parseSockaddr(addr, defaultPort = 0) {
|
|
8
|
-
let match
|
|
9
|
-
if (/^[0-9]+$/.test(addr)) return { host: '::', port: parseInt(addr, 10) }
|
|
10
|
-
|
|
11
|
-
const lastColon = addr.lastIndexOf(':')
|
|
12
|
-
if (lastColon !== -1) {
|
|
13
|
-
const host = addr.slice(0, lastColon)
|
|
14
|
-
const port = addr.slice(lastColon + 1)
|
|
15
|
-
|
|
16
|
-
if (host.includes(':') && /^\d+$/.test(port) && net.isIP(host) === 6) {
|
|
17
|
-
return { host: host.toLowerCase(), port: parseInt(port, 10) }
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
if (net.isIP(addr) === 6) return { host: addr.toLowerCase(), port: defaultPort }
|
|
21
|
-
|
|
22
|
-
if ((match = /^(\d{1,3}(?:\.\d{1,3}){3})(?::(\d+))?$/.exec(addr)))
|
|
23
|
-
return { host: match[1], port: match[2] !== undefined ? parseInt(match[2], 10) : defaultPort }
|
|
24
|
-
if ((match = /^\[([0-9a-fA-F:]+)\](?::(\d+))?$/.exec(addr)))
|
|
25
|
-
return { host: match[1].toLowerCase(), port: match[2] !== undefined ? parseInt(match[2], 10) : defaultPort }
|
|
26
|
-
if (
|
|
27
|
-
(match =
|
|
28
|
-
/^([a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?)*)(?::(\d+))?$/.exec(
|
|
29
|
-
addr,
|
|
30
|
-
))
|
|
31
|
-
)
|
|
32
|
-
return { host: match[1].toLowerCase(), port: match[2] !== undefined ? parseInt(match[2], 10) : defaultPort }
|
|
33
|
-
if (addr.includes('/')) return { path: addr }
|
|
34
|
-
throw new Error(`Invalid socket address ${addr}`)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
module.exports = function endpoint(addr, defaultPort) {
|
|
38
|
-
try {
|
|
39
|
-
if ('string' === typeof addr || 'number' === typeof addr) {
|
|
40
|
-
addr = parseSockaddr(addr, defaultPort)
|
|
41
|
-
const match = /^(.*):([0-7]{3})$/.exec(addr.path || '')
|
|
42
|
-
if (match) {
|
|
43
|
-
addr.path = match[1]
|
|
44
|
-
addr.mode = match[2]
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
} catch (err) {
|
|
48
|
-
// Return the parse exception instead of throwing it
|
|
49
|
-
return err
|
|
50
|
-
}
|
|
51
|
-
return new Endpoint(addr)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
class Endpoint {
|
|
55
|
-
constructor(addr) {
|
|
56
|
-
if (addr.path) {
|
|
57
|
-
this.path = addr.path
|
|
58
|
-
if (addr.mode) this.mode = addr.mode
|
|
59
|
-
} else {
|
|
60
|
-
// Handle server.address() return as well as parsed host/port
|
|
61
|
-
const host = addr.address || addr.host || '::0'
|
|
62
|
-
// Normalize '::' to '::0'
|
|
63
|
-
this.host = '::' === host ? '::0' : host
|
|
64
|
-
this.port = parseInt(addr.port, 10)
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
toString() {
|
|
69
|
-
if (this.mode) return `${this.path}:${this.mode}`
|
|
70
|
-
if (this.path) return this.path
|
|
71
|
-
if (this.host.includes(':')) return `[${this.host}]:${this.port}`
|
|
72
|
-
return `${this.host}:${this.port}`
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Make server listen on this endpoint, w/optional options
|
|
76
|
-
async bind(server, opts) {
|
|
77
|
-
opts = { ...opts }
|
|
78
|
-
|
|
79
|
-
const mode = this.mode ? parseInt(this.mode, 8) : false
|
|
80
|
-
if (this.path) {
|
|
81
|
-
opts.path = this.path
|
|
82
|
-
await fs.rm(this.path, { force: true }) // errors are ignored when force is true
|
|
83
|
-
} else {
|
|
84
|
-
opts.host = this.host
|
|
85
|
-
opts.port = this.port
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return new Promise((resolve, reject) => {
|
|
89
|
-
server.listen(opts, async (err) => {
|
|
90
|
-
if (err) return reject(err)
|
|
91
|
-
if (mode) await fs.chmod(opts.path, mode)
|
|
92
|
-
resolve()
|
|
93
|
-
})
|
|
94
|
-
})
|
|
95
|
-
}
|
|
96
|
-
}
|
package/host_pool.js
DELETED
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const net = require('node:net')
|
|
4
|
-
const utils = require('haraka-utils')
|
|
5
|
-
|
|
6
|
-
/* HostPool:
|
|
7
|
-
*
|
|
8
|
-
* Starts with a pool of backend hosts from a "forwarding_host_pool"
|
|
9
|
-
* configuration that looks like this (port defaults to 25 if not set):
|
|
10
|
-
*
|
|
11
|
-
* 1.1.1.1:11, 2.2.2.2:22, 3.3.3.3:33
|
|
12
|
-
*
|
|
13
|
-
* It randomizes the list and then gives then out sequentially (for
|
|
14
|
-
* predictability).
|
|
15
|
-
*
|
|
16
|
-
* If failed() is called with one of the hosts, we mark it down for retry_secs
|
|
17
|
-
* and don't give it out again until that period has passed.
|
|
18
|
-
*
|
|
19
|
-
* If *all* the hosts have been marked down, ignore the marks and give
|
|
20
|
-
* out the next host. That's to keep a random short-lived but widespread
|
|
21
|
-
* network failure from taking the whole system down.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
const logger = require('./logger')
|
|
25
|
-
|
|
26
|
-
class HostPool {
|
|
27
|
-
// takes a comma/space-separated list of ip:ports
|
|
28
|
-
// 1.1.1.1:22, 3.3.3.3:44
|
|
29
|
-
constructor(hostports_str, retry_secs) {
|
|
30
|
-
const hosts = (hostports_str || '')
|
|
31
|
-
.trim()
|
|
32
|
-
.split(/[\s,]+/)
|
|
33
|
-
.map((hostport) => {
|
|
34
|
-
const splithost = hostport.split(/:/)
|
|
35
|
-
if (!splithost[1]) {
|
|
36
|
-
splithost[1] = 25
|
|
37
|
-
}
|
|
38
|
-
return {
|
|
39
|
-
host: splithost[0],
|
|
40
|
-
port: splithost[1],
|
|
41
|
-
}
|
|
42
|
-
})
|
|
43
|
-
this.hostports_str = hostports_str
|
|
44
|
-
this.hosts = utils.shuffle(hosts)
|
|
45
|
-
this.dead_hosts = {} // hostport => true/false
|
|
46
|
-
this.last_i = 0 // the last one we checked
|
|
47
|
-
this.retry_secs = retry_secs || 10
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/* failed
|
|
51
|
-
*
|
|
52
|
-
* Part of the external API for this module. Call it when you see a failure to
|
|
53
|
-
* this backend host and it'll come out of the pool and put into the recheck
|
|
54
|
-
* timer.
|
|
55
|
-
*/
|
|
56
|
-
failed(host, port) {
|
|
57
|
-
const self = this
|
|
58
|
-
const key = `${host}:${port}`
|
|
59
|
-
const retry_msecs = self.retry_secs * 1000
|
|
60
|
-
self.dead_hosts[key] = true
|
|
61
|
-
|
|
62
|
-
function cb_if_still_dead() {
|
|
63
|
-
logger.warn(`${host} ${key} is still dead, will retry in ${self.retry_secs} secs`)
|
|
64
|
-
self.dead_hosts[key] = true
|
|
65
|
-
// console.log(1);
|
|
66
|
-
setTimeout(() => {
|
|
67
|
-
self.probe_dead_host(host, port, cb_if_still_dead, cb_if_alive)
|
|
68
|
-
}, retry_msecs)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function cb_if_alive() {
|
|
72
|
-
// console.log(2);
|
|
73
|
-
logger.info(`${host} ${key} is back! adding back into pool`)
|
|
74
|
-
delete self.dead_hosts[key]
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
setTimeout(() => {
|
|
78
|
-
self.probe_dead_host(host, port, cb_if_still_dead, cb_if_alive)
|
|
79
|
-
}, retry_msecs)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/* probe_dead_host
|
|
83
|
-
*
|
|
84
|
-
* When the timer fires, we'll ping the host, and if it's still dead we'll
|
|
85
|
-
* update the dead_hosts list. If it's back online, we just don't touch the
|
|
86
|
-
* dead_hosts lists, and the next time get_host() is called, it'll be in the
|
|
87
|
-
* mix.
|
|
88
|
-
*/
|
|
89
|
-
probe_dead_host(host, port, cb_if_still_dead, cb_if_alive) {
|
|
90
|
-
logger.info(`probing dead host ${host}:${port}`)
|
|
91
|
-
|
|
92
|
-
const connect_timeout_ms = 200 // keep it snappy
|
|
93
|
-
let s
|
|
94
|
-
try {
|
|
95
|
-
s = this.get_socket()
|
|
96
|
-
s.setTimeout(connect_timeout_ms, () => {
|
|
97
|
-
// nobody home, it's still dead
|
|
98
|
-
s.destroy()
|
|
99
|
-
cb_if_still_dead()
|
|
100
|
-
})
|
|
101
|
-
s.on('error', (e) => {
|
|
102
|
-
// silently catch all errors - assume the port is closed
|
|
103
|
-
s.destroy()
|
|
104
|
-
cb_if_still_dead()
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
s.connect(port, host, () => {
|
|
108
|
-
cb_if_alive()
|
|
109
|
-
s.destroy() // will this conflict with setTimeout's s.destroy?
|
|
110
|
-
})
|
|
111
|
-
} catch (e) {
|
|
112
|
-
// only way to catch run-time javascript errors in here;
|
|
113
|
-
console.log(`ERROR in probe_dead_host, got error ${e}`)
|
|
114
|
-
throw e
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/* get_socket
|
|
119
|
-
*
|
|
120
|
-
* so we can override in unit test
|
|
121
|
-
*/
|
|
122
|
-
get_socket() {
|
|
123
|
-
return new net.Socket()
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/* get_host
|
|
127
|
-
*
|
|
128
|
-
* This approach borrowed from the danga mogilefs client code
|
|
129
|
-
*
|
|
130
|
-
* If all the hosts look dead, it returns the next one it would have tried
|
|
131
|
-
* anyway. That should make it more forgiving about transient but widespread
|
|
132
|
-
* network problems that make all the hosts look dead.
|
|
133
|
-
*/
|
|
134
|
-
get_host() {
|
|
135
|
-
let host
|
|
136
|
-
let found
|
|
137
|
-
|
|
138
|
-
let first_i = this.last_i + 1
|
|
139
|
-
if (first_i >= this.hosts.length) {
|
|
140
|
-
first_i = 0
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
for (let i = 0; i < this.hosts.length; ++i) {
|
|
144
|
-
let j = i + first_i
|
|
145
|
-
if (j >= this.hosts.length) {
|
|
146
|
-
j -= this.hosts.length
|
|
147
|
-
}
|
|
148
|
-
host = this.hosts[j]
|
|
149
|
-
const key = `${host.host}:${host.port}`
|
|
150
|
-
if (this.dead_hosts[key]) {
|
|
151
|
-
continue
|
|
152
|
-
}
|
|
153
|
-
this.last_i = j
|
|
154
|
-
found = true
|
|
155
|
-
break
|
|
156
|
-
}
|
|
157
|
-
if (found) {
|
|
158
|
-
return host
|
|
159
|
-
} else {
|
|
160
|
-
logger.warn(
|
|
161
|
-
`no working hosts found, retrying a dead one, config (probably from smtp_forward.forwarding_host_pool) is '${this.hostports_str}'`,
|
|
162
|
-
)
|
|
163
|
-
this.last_i = first_i
|
|
164
|
-
return this.hosts[first_i]
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
module.exports = HostPool
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const fs = require('node:fs')
|
|
4
|
-
|
|
5
|
-
class FsyncWriteStream extends fs.WriteStream {
|
|
6
|
-
constructor(path, options) {
|
|
7
|
-
super(path, options)
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
close(cb) {
|
|
11
|
-
const self = this
|
|
12
|
-
if (cb) this.once('close', cb)
|
|
13
|
-
|
|
14
|
-
if (this.closed || 'number' !== typeof this.fd) {
|
|
15
|
-
if ('number' !== typeof this.fd) {
|
|
16
|
-
this.once('open', close)
|
|
17
|
-
return
|
|
18
|
-
}
|
|
19
|
-
return setImmediate(this.emit.bind(this, 'close'))
|
|
20
|
-
}
|
|
21
|
-
this.closed = true
|
|
22
|
-
close()
|
|
23
|
-
|
|
24
|
-
function close(fd) {
|
|
25
|
-
fs.fsync(fd || self.fd, (er) => {
|
|
26
|
-
if (er) {
|
|
27
|
-
self.emit('error', er)
|
|
28
|
-
return
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
fs.close(fd || self.fd, (err) => {
|
|
32
|
-
if (err) {
|
|
33
|
-
self.emit('error', err)
|
|
34
|
-
} else {
|
|
35
|
-
self.emit('close')
|
|
36
|
-
}
|
|
37
|
-
})
|
|
38
|
-
self.fd = null
|
|
39
|
-
})
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
module.exports = FsyncWriteStream
|
package/outbound/timer_queue.js
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const logger = require('../logger')
|
|
4
|
-
|
|
5
|
-
class TQTimer {
|
|
6
|
-
constructor(id, fire_time, cb) {
|
|
7
|
-
this.id = id
|
|
8
|
-
this.fire_time = fire_time
|
|
9
|
-
this.cb = cb
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
cancel() {
|
|
13
|
-
this.cb = null
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
class TimerQueue {
|
|
18
|
-
constructor(interval = 1000) {
|
|
19
|
-
this.name = 'outbound/timer_queue'
|
|
20
|
-
this.queue = []
|
|
21
|
-
this.interval_timer = setInterval(() => {
|
|
22
|
-
this.fire()
|
|
23
|
-
}, interval)
|
|
24
|
-
this.interval_timer.unref() // allow server to exit
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
add(id, ms, cb) {
|
|
28
|
-
const fire_time = Date.now() + ms
|
|
29
|
-
|
|
30
|
-
const timer = new TQTimer(id, fire_time, cb)
|
|
31
|
-
|
|
32
|
-
if (this.queue.length === 0 || fire_time >= this.queue[this.queue.length - 1].fire_time) {
|
|
33
|
-
this.queue.push(timer)
|
|
34
|
-
return timer
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
for (let i = 0; i < this.queue.length; i++) {
|
|
38
|
-
if (this.queue[i].fire_time > fire_time) {
|
|
39
|
-
this.queue.splice(i, 0, timer)
|
|
40
|
-
return timer
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
throw 'Should never get here'
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
discard(id) {
|
|
48
|
-
for (let i = 0; i < this.queue.length; i++) {
|
|
49
|
-
if (this.queue[i].id === id) {
|
|
50
|
-
this.queue[i].cancel()
|
|
51
|
-
return this.queue.splice(i, 1)
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
throw `${id} not found`
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
fire() {
|
|
59
|
-
if (this.queue.length === 0) return
|
|
60
|
-
|
|
61
|
-
const now = Date.now()
|
|
62
|
-
|
|
63
|
-
while (this.queue.length && this.queue[0].fire_time <= now) {
|
|
64
|
-
const to_run = this.queue.shift()
|
|
65
|
-
if (to_run.cb) to_run.cb()
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
length() {
|
|
70
|
-
return this.queue.length
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
drain() {
|
|
74
|
-
logger.debug(this, `Draining ${this.queue.length} items from the queue`)
|
|
75
|
-
while (this.queue.length) {
|
|
76
|
-
const to_run = this.queue.shift()
|
|
77
|
-
if (to_run.cb) to_run.cb()
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
shutdown() {
|
|
82
|
-
clearInterval(this.interval_timer)
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
module.exports = TimerQueue
|
package/rfc1869.js
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
// RFC 1869 command parser
|
|
3
|
-
|
|
4
|
-
// 6. MAIL FROM and RCPT TO Parameters
|
|
5
|
-
// [...]
|
|
6
|
-
//
|
|
7
|
-
// esmtp-cmd ::= inner-esmtp-cmd [SP esmtp-parameters] CR LF
|
|
8
|
-
// esmtp-parameters ::= esmtp-parameter *(SP esmtp-parameter)
|
|
9
|
-
// esmtp-parameter ::= esmtp-keyword ["=" esmtp-value]
|
|
10
|
-
// esmtp-keyword ::= (ALPHA / DIGIT) *(ALPHA / DIGIT / "-")
|
|
11
|
-
//
|
|
12
|
-
// ; syntax and values depend on esmtp-keyword
|
|
13
|
-
// esmtp-value ::= 1*<any CHAR excluding "=", SP, and all
|
|
14
|
-
// control characters (US ASCII 0-31
|
|
15
|
-
// inclusive)>
|
|
16
|
-
//
|
|
17
|
-
// ; The following commands are extended to
|
|
18
|
-
// ; accept extended parameters.
|
|
19
|
-
// inner-esmtp-cmd ::= ("MAIL FROM:" reverse-path) /
|
|
20
|
-
// ("RCPT TO:" forward-path)
|
|
21
|
-
|
|
22
|
-
/* eslint no-control-regex: 0 */
|
|
23
|
-
const chew_regexp = /\s+([A-Za-z0-9][A-Za-z0-9-]*(?:=[^= \x00-\x1f]+)?)$/
|
|
24
|
-
|
|
25
|
-
exports.parse = (type, line, strict) => {
|
|
26
|
-
let params = []
|
|
27
|
-
line = new String(line).replace(/\s*$/, '')
|
|
28
|
-
if (type === 'mail') {
|
|
29
|
-
line = line.replace(strict ? /from:/i : /from:\s*/i, '')
|
|
30
|
-
} else {
|
|
31
|
-
line = line.replace(strict ? /to:/i : /to:\s*/i, '')
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
while (1) {
|
|
35
|
-
const old_length = line.length
|
|
36
|
-
line = line.replace(chew_regexp, (str, p1) => {
|
|
37
|
-
params.push(p1)
|
|
38
|
-
return ''
|
|
39
|
-
})
|
|
40
|
-
if (old_length === line.length) break
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
params = params.reverse()
|
|
44
|
-
|
|
45
|
-
// the above will "fail" (i.e. all of the line in params) on
|
|
46
|
-
// some addresses without <> like
|
|
47
|
-
// MAIL FROM: user=name@example.net
|
|
48
|
-
// or RCPT TO: postmaster
|
|
49
|
-
|
|
50
|
-
// let's see if $line contains nothing and use the first value as address:
|
|
51
|
-
if (line.length) {
|
|
52
|
-
// parameter syntax error, i.e. not all of the arguments were
|
|
53
|
-
// stripped by the while() loop:
|
|
54
|
-
if (line.match(/@.*\s/)) {
|
|
55
|
-
throw new Error(`Syntax error in parameters ("${line}")`)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
params.unshift(line)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
line = params.shift() || ''
|
|
62
|
-
if (strict) {
|
|
63
|
-
if (!line.match(/^<.*>$/)) {
|
|
64
|
-
throw new Error(`Invalid format of ${type} command: ${line}`)
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (type === 'mail') {
|
|
69
|
-
if (!line.length) {
|
|
70
|
-
return ['<>'] // 'MAIL FROM:' --> 'MAIL FROM:<>'
|
|
71
|
-
}
|
|
72
|
-
if (line.match(/@.*\s/)) {
|
|
73
|
-
throw new Error('Syntax error in parameters')
|
|
74
|
-
}
|
|
75
|
-
} else {
|
|
76
|
-
// console.log(`Looking at ${line}`);
|
|
77
|
-
if (line.match(/@.*\s/)) {
|
|
78
|
-
throw new Error('Syntax error in parameters')
|
|
79
|
-
}
|
|
80
|
-
if (line.match(/\s/)) {
|
|
81
|
-
throw new Error('Syntax error in parameters')
|
|
82
|
-
}
|
|
83
|
-
if (line.match(/@/)) {
|
|
84
|
-
if (!line.match(/^<.*>$/)) line = `<${line}>`
|
|
85
|
-
} else if (!line.match(/^<(postmaster|abuse)>$/i)) {
|
|
86
|
-
throw new Error(`Syntax error in address: ${line}`)
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
params.unshift(line)
|
|
91
|
-
|
|
92
|
-
return params
|
|
93
|
-
}
|
package/test/endpoint.js
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const { describe, it, beforeEach, afterEach } = require('node:test')
|
|
4
|
-
const assert = require('node:assert')
|
|
5
|
-
|
|
6
|
-
const mock = require('mock-require')
|
|
7
|
-
const endpoint = require('../endpoint')
|
|
8
|
-
|
|
9
|
-
describe('endpoint', () => {
|
|
10
|
-
it('toString()', () => {
|
|
11
|
-
assert.equal(endpoint(25), '[::0]:25')
|
|
12
|
-
assert.equal(endpoint('10.0.0.3', 42), '10.0.0.3:42')
|
|
13
|
-
assert.equal(endpoint('/foo/bar.sock'), '/foo/bar.sock')
|
|
14
|
-
assert.equal(endpoint('/foo/bar.sock:770'), '/foo/bar.sock:770')
|
|
15
|
-
assert.equal(endpoint({ address: '::0', port: 80 }), '[::0]:80')
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
describe('parse', () => {
|
|
19
|
-
it('Number as port', () => {
|
|
20
|
-
assert.deepEqual(endpoint(25), { host: '::0', port: 25 })
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
it('Unbracketed IPv6 host uses default port', () => {
|
|
24
|
-
assert.deepEqual(endpoint('::0', 25), {
|
|
25
|
-
host: '::0',
|
|
26
|
-
port: 25,
|
|
27
|
-
})
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('Unbracketed IPv6 host:port parses correctly (PR #3552 compatibility)', () => {
|
|
31
|
-
assert.deepEqual(endpoint('::0:25'), {
|
|
32
|
-
host: '::0',
|
|
33
|
-
port: 25,
|
|
34
|
-
})
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
it('Default port if only host', () => {
|
|
38
|
-
assert.deepEqual(endpoint('10.0.0.3', 42), {
|
|
39
|
-
host: '10.0.0.3',
|
|
40
|
-
port: 42,
|
|
41
|
-
})
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('Bracketed IPv6 host is normalized to lowercase', () => {
|
|
45
|
-
assert.deepEqual(endpoint('[ABCD::EF01]:2525'), {
|
|
46
|
-
host: 'abcd::ef01',
|
|
47
|
-
port: 2525,
|
|
48
|
-
})
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it('Unix socket', () => {
|
|
52
|
-
assert.deepEqual(endpoint('/foo/bar.sock'), {
|
|
53
|
-
path: '/foo/bar.sock',
|
|
54
|
-
})
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
it('Unix socket w/mode', () => {
|
|
58
|
-
assert.deepEqual(endpoint('/foo/bar.sock:770'), {
|
|
59
|
-
path: '/foo/bar.sock',
|
|
60
|
-
mode: '770',
|
|
61
|
-
})
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it('Invalid unbracketed IPv6 host with non-numeric tail returns Error', () => {
|
|
65
|
-
const ep = endpoint('::0:port')
|
|
66
|
-
assert.equal(ep instanceof Error, true)
|
|
67
|
-
assert.match(ep.message, /Invalid socket address/)
|
|
68
|
-
})
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
describe('bind()', () => {
|
|
72
|
-
beforeEach(() => {
|
|
73
|
-
// Mock filesystem and log server + fs method calls
|
|
74
|
-
const modes = (this.modes = {})
|
|
75
|
-
const log = (this.log = [])
|
|
76
|
-
|
|
77
|
-
this.server = {
|
|
78
|
-
listen(opts, cb) {
|
|
79
|
-
log.push(['listen', opts])
|
|
80
|
-
if (cb) cb()
|
|
81
|
-
},
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
this.mockfs = {
|
|
85
|
-
chmod(path, mode, ...args) {
|
|
86
|
-
log.push(['chmod', path, mode, ...args])
|
|
87
|
-
modes[path] = mode
|
|
88
|
-
},
|
|
89
|
-
rm(path, ...args) {
|
|
90
|
-
log.push(['rm', path, ...args])
|
|
91
|
-
},
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
mock('node:fs/promises', this.mockfs)
|
|
95
|
-
this.endpoint = mock.reRequire('../endpoint')
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
afterEach(() => {
|
|
99
|
-
mock.stop('node:fs/promises')
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
it('IP socket', async () => {
|
|
103
|
-
await this.endpoint('10.0.0.3:42').bind(this.server, {
|
|
104
|
-
backlog: 19,
|
|
105
|
-
})
|
|
106
|
-
assert.deepEqual(this.log, [['listen', { host: '10.0.0.3', port: 42, backlog: 19 }]])
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
it('Unix socket', async () => {
|
|
110
|
-
await this.endpoint('/foo/bar.sock').bind(this.server, {
|
|
111
|
-
readableAll: true,
|
|
112
|
-
})
|
|
113
|
-
assert.deepEqual(this.log, [
|
|
114
|
-
['rm', '/foo/bar.sock', { force: true }],
|
|
115
|
-
['listen', { path: '/foo/bar.sock', readableAll: true }],
|
|
116
|
-
])
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
it('Unix socket w/mode', async () => {
|
|
120
|
-
await this.endpoint('/foo/bar.sock:764').bind(this.server)
|
|
121
|
-
assert.deepEqual(this.log, [
|
|
122
|
-
['rm', '/foo/bar.sock', { force: true }],
|
|
123
|
-
['listen', { path: '/foo/bar.sock' }],
|
|
124
|
-
['chmod', '/foo/bar.sock', 0o764],
|
|
125
|
-
])
|
|
126
|
-
})
|
|
127
|
-
})
|
|
128
|
-
})
|