Haraka 3.1.6 → 3.2.0
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 +39 -1
- package/CONTRIBUTORS.md +8 -8
- package/Plugins.md +99 -99
- package/address.js +53 -0
- package/bin/haraka +1 -1
- package/config/smtp_forward.ini +10 -0
- package/config/smtp_proxy.ini +10 -0
- package/connection.js +28 -11
- package/docs/Outbound.md +1 -1
- package/docs/Transaction.md +1 -1
- package/docs/plugins/queue/smtp_forward.md +19 -3
- package/docs/plugins/queue/smtp_proxy.md +10 -2
- package/docs/plugins/status.md +21 -5
- package/haraka.js +1 -1
- package/outbound/hmail.js +41 -41
- package/outbound/index.js +5 -5
- package/outbound/queue.js +1 -1
- package/outbound/tls.js +2 -43
- package/package.json +48 -48
- package/plugins/auth/auth_base.js +9 -3
- package/plugins/auth/auth_proxy.js +14 -11
- package/plugins/block_me.js +6 -4
- 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 +14 -6
- package/plugins/queue/smtp_proxy.js +14 -3
- package/plugins/rcpt_to.host_list_base.js +1 -1
- package/plugins/record_envelope_addresses.js +2 -2
- package/plugins/status.js +34 -5
- 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 +25 -1
- package/test/fixtures/util_hmailitem.js +1 -1
- package/test/outbound/bounce_net_errors.js +3 -2
- package/test/outbound/index.js +2 -2
- package/test/plugins/auth/auth_base.js +1 -1
- 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 +43 -7
- package/test/plugins/queue/smtp_proxy.js +139 -0
- package/test/plugins/rcpt_to.host_list_base.js +1 -1
- package/test/plugins/rcpt_to.in_host_list.js +1 -1
- package/test/plugins/record_envelope_addresses.js +2 -2
- package/test/plugins/reseed_rng.js +34 -0
- package/test/plugins/status.js +71 -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 +46 -13
- package/test/tls_socket.js +82 -0
- package/tls_socket.js +50 -0
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"server",
|
|
10
10
|
"email"
|
|
11
11
|
],
|
|
12
|
-
"version": "3.
|
|
12
|
+
"version": "3.2.0",
|
|
13
13
|
"homepage": "http://haraka.github.io",
|
|
14
14
|
"repository": {
|
|
15
15
|
"type": "git",
|
|
@@ -20,58 +20,57 @@
|
|
|
20
20
|
"node": ">=20"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"address
|
|
24
|
-
"
|
|
25
|
-
"haraka-
|
|
26
|
-
"haraka-
|
|
27
|
-
"haraka-
|
|
28
|
-
"haraka-
|
|
29
|
-
"haraka-
|
|
30
|
-
"haraka-
|
|
31
|
-
"haraka-
|
|
32
|
-
"haraka-
|
|
33
|
-
"haraka-
|
|
34
|
-
"haraka-utils": "^1.1.4",
|
|
23
|
+
"@haraka/email-address": "~3.1.3",
|
|
24
|
+
"haraka-config": "~1.6.0",
|
|
25
|
+
"haraka-constants": "~1.0.7",
|
|
26
|
+
"haraka-dsn": "~1.2.0",
|
|
27
|
+
"haraka-email-message": "~1.3.3",
|
|
28
|
+
"haraka-net-utils": "~1.8.2",
|
|
29
|
+
"haraka-notes": "~1.1.3",
|
|
30
|
+
"haraka-plugin-redis": "~2.0.11",
|
|
31
|
+
"haraka-results": "~2.3.0",
|
|
32
|
+
"haraka-tld": "~1.3.4",
|
|
33
|
+
"haraka-utils": "~1.1.4",
|
|
35
34
|
"ipaddr.js": "~2.4.0",
|
|
36
|
-
"node-gyp": "
|
|
37
|
-
"nopt": "
|
|
35
|
+
"node-gyp": "~12.3.0",
|
|
36
|
+
"nopt": "~10.0.0",
|
|
38
37
|
"redis": "~5.12.1",
|
|
39
|
-
"semver": "
|
|
38
|
+
"semver": "~7.8.0"
|
|
40
39
|
},
|
|
41
40
|
"optionalDependencies": {
|
|
42
|
-
"@haraka/ocsp": "
|
|
43
|
-
"haraka-plugin-access": "
|
|
44
|
-
"haraka-plugin-aliases": "
|
|
45
|
-
"haraka-plugin-asn": "
|
|
46
|
-
"haraka-plugin-attachment": "
|
|
47
|
-
"haraka-plugin-bounce": "
|
|
48
|
-
"haraka-plugin-clamd": "
|
|
49
|
-
"haraka-plugin-dcc": "
|
|
50
|
-
"haraka-plugin-dkim": "
|
|
51
|
-
"haraka-plugin-dns-list": "
|
|
52
|
-
"haraka-plugin-early_talker": "
|
|
53
|
-
"haraka-plugin-fcrdns": "
|
|
54
|
-
"haraka-plugin-geoip": "
|
|
55
|
-
"haraka-plugin-greylist": "
|
|
56
|
-
"haraka-plugin-headers": "
|
|
57
|
-
"haraka-plugin-helo.checks": "
|
|
58
|
-
"haraka-plugin-karma": "
|
|
59
|
-
"haraka-plugin-known-senders": "
|
|
60
|
-
"haraka-plugin-limit": "
|
|
61
|
-
"haraka-plugin-mail_from.is_resolvable": "
|
|
62
|
-
"haraka-plugin-messagesniffer": "
|
|
63
|
-
"haraka-plugin-qmail-deliverable": "
|
|
64
|
-
"haraka-plugin-relay": "
|
|
65
|
-
"haraka-plugin-rspamd": "
|
|
66
|
-
"haraka-plugin-spamassassin": "
|
|
67
|
-
"haraka-plugin-spf": "
|
|
68
|
-
"haraka-plugin-syslog": "
|
|
69
|
-
"haraka-plugin-uribl": "
|
|
41
|
+
"@haraka/ocsp": "~1.2.0",
|
|
42
|
+
"haraka-plugin-access": "~1.2.0",
|
|
43
|
+
"haraka-plugin-aliases": "~1.0.3",
|
|
44
|
+
"haraka-plugin-asn": "~2.1.0",
|
|
45
|
+
"haraka-plugin-attachment": "~1.2.0",
|
|
46
|
+
"haraka-plugin-bounce": "~2.1.2",
|
|
47
|
+
"haraka-plugin-clamd": "~1.0.2",
|
|
48
|
+
"haraka-plugin-dcc": "~1.0.3",
|
|
49
|
+
"haraka-plugin-dkim": "~1.1.2",
|
|
50
|
+
"haraka-plugin-dns-list": "~1.2.4",
|
|
51
|
+
"haraka-plugin-early_talker": "~1.0.2",
|
|
52
|
+
"haraka-plugin-fcrdns": "~1.1.2",
|
|
53
|
+
"haraka-plugin-geoip": "~1.1.2",
|
|
54
|
+
"haraka-plugin-greylist": "~1.1.1",
|
|
55
|
+
"haraka-plugin-headers": "~1.1.2",
|
|
56
|
+
"haraka-plugin-helo.checks": "~1.1.1",
|
|
57
|
+
"haraka-plugin-karma": "~2.4.1",
|
|
58
|
+
"haraka-plugin-known-senders": "~1.1.4",
|
|
59
|
+
"haraka-plugin-limit": "~1.2.7",
|
|
60
|
+
"haraka-plugin-mail_from.is_resolvable": "~1.2.0",
|
|
61
|
+
"haraka-plugin-messagesniffer": "~1.0.1",
|
|
62
|
+
"haraka-plugin-qmail-deliverable": "~1.3.5",
|
|
63
|
+
"haraka-plugin-relay": "~1.0.2",
|
|
64
|
+
"haraka-plugin-rspamd": "~1.5.0",
|
|
65
|
+
"haraka-plugin-spamassassin": "~1.0.4",
|
|
66
|
+
"haraka-plugin-spf": "~1.2.11",
|
|
67
|
+
"haraka-plugin-syslog": "~1.1.0",
|
|
68
|
+
"haraka-plugin-uribl": "~1.0.10"
|
|
70
69
|
},
|
|
71
70
|
"devDependencies": {
|
|
72
|
-
"@haraka/eslint-config": "
|
|
73
|
-
"haraka-test-fixtures": "
|
|
74
|
-
"mock-require": "
|
|
71
|
+
"@haraka/eslint-config": "~2.0.4",
|
|
72
|
+
"haraka-test-fixtures": "~1.6.0",
|
|
73
|
+
"mock-require": "~3.0.3"
|
|
75
74
|
},
|
|
76
75
|
"bugs": {
|
|
77
76
|
"mail": "haraka.mail@gmail.com",
|
|
@@ -90,7 +89,8 @@
|
|
|
90
89
|
"test": "sh ./run_tests",
|
|
91
90
|
"versions": "npx npm-dep-mgr check",
|
|
92
91
|
"versions:fix": "npx npm-dep-mgr update",
|
|
93
|
-
"test:coverage": "
|
|
92
|
+
"test:coverage": "node --test --test-concurrency=1 --experimental-test-coverage",
|
|
93
|
+
"test:coverage:lcov": "mkdir -p coverage && node --test --test-concurrency=1 --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=coverage/lcov.info"
|
|
94
94
|
},
|
|
95
95
|
"prettier": {
|
|
96
96
|
"singleQuote": true,
|
|
@@ -99,6 +99,12 @@ exports.check_user = function (next, connection, credentials, method) {
|
|
|
99
99
|
(typeof opts === 'object' ? opts.message : opts) ||
|
|
100
100
|
(valid ? '2.7.0 Authentication successful' : '5.7.8 Authentication failed')
|
|
101
101
|
|
|
102
|
+
// The AUTH username is attacker-controlled (base64-decoded). Strip
|
|
103
|
+
// control chars before it is stored in notes or emitted into the
|
|
104
|
+
// Authentication-Results header (header injection).
|
|
105
|
+
// eslint-disable-next-line no-control-regex
|
|
106
|
+
const safe_user = String(credentials[0] ?? '').replace(/[\x00-\x1f\x7f]/g, '')
|
|
107
|
+
|
|
102
108
|
if (valid) {
|
|
103
109
|
connection.relaying = true
|
|
104
110
|
connection.results.add({ name: 'relay' }, { pass: plugin.name })
|
|
@@ -108,14 +114,14 @@ exports.check_user = function (next, connection, credentials, method) {
|
|
|
108
114
|
{
|
|
109
115
|
pass: plugin.name,
|
|
110
116
|
method,
|
|
111
|
-
user:
|
|
117
|
+
user: safe_user,
|
|
112
118
|
},
|
|
113
119
|
)
|
|
114
120
|
|
|
115
121
|
connection.respond(status_code, status_message, () => {
|
|
116
122
|
connection.authheader = '(authenticated bits=0)\n'
|
|
117
123
|
connection.auth_results(`auth=pass (${method.toLowerCase()})`)
|
|
118
|
-
connection.notes.auth_user =
|
|
124
|
+
connection.notes.auth_user = safe_user
|
|
119
125
|
if (!plugin.blankout_password) connection.notes.auth_passwd = credentials[1]
|
|
120
126
|
next(OK)
|
|
121
127
|
})
|
|
@@ -133,7 +139,7 @@ exports.check_user = function (next, connection, credentials, method) {
|
|
|
133
139
|
}
|
|
134
140
|
connection.lognotice(plugin, `delaying for ${delay} seconds`)
|
|
135
141
|
// here we include the username, as shown in RFC 5451 example
|
|
136
|
-
connection.auth_results(`auth=fail (${method.toLowerCase()}) smtp.auth=${
|
|
142
|
+
connection.auth_results(`auth=fail (${method.toLowerCase()}) smtp.auth=${safe_user}`)
|
|
137
143
|
setTimeout(() => {
|
|
138
144
|
connection.respond(status_code, status_message, () => {
|
|
139
145
|
connection.reset_transaction(() => next(OK))
|
|
@@ -93,7 +93,9 @@ exports.try_auth_proxy = function (connection, hosts, user, passwd, cb) {
|
|
|
93
93
|
if (cmd === 'dot') {
|
|
94
94
|
line = '.'
|
|
95
95
|
}
|
|
96
|
-
|
|
96
|
+
// Don't leak proxied SASL credentials (AUTH PLAIN <base64>) to logs
|
|
97
|
+
const safe = line.replace(/^(AUTH\s+\S+\s+).+$/i, '$1[redacted]')
|
|
98
|
+
connection.logprotocol(self, `C: ${safe}`)
|
|
97
99
|
command = cmd.toLowerCase()
|
|
98
100
|
this.write(`${line}\r\n`)
|
|
99
101
|
// Clear response buffer from previous command
|
|
@@ -128,18 +130,19 @@ exports.try_auth_proxy = function (connection, hosts, user, passwd, cb) {
|
|
|
128
130
|
for (const i in response) {
|
|
129
131
|
if (/^STARTTLS/.test(response[i])) {
|
|
130
132
|
if (secure) continue // silly remote, we've already upgraded
|
|
131
|
-
//
|
|
133
|
+
// Opportunistic TLS: a client does not need its own
|
|
134
|
+
// certificate to negotiate TLS, so always STARTTLS when
|
|
135
|
+
// the backend offers it. The local key/cert are only
|
|
136
|
+
// attached if configured (mutual TLS), not required.
|
|
132
137
|
key = self.config.get(self.tls_cfg.main.key || 'tls_key.pem', 'binary')
|
|
133
138
|
cert = self.config.get(self.tls_cfg.main.cert || 'tls_cert.pem', 'binary')
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
return
|
|
142
|
-
}
|
|
139
|
+
this.on('secure', () => {
|
|
140
|
+
if (secure) return
|
|
141
|
+
secure = true
|
|
142
|
+
socket.send_command('EHLO', connection.local.host)
|
|
143
|
+
})
|
|
144
|
+
socket.send_command('STARTTLS')
|
|
145
|
+
return
|
|
143
146
|
} else if (/^AUTH /.test(response[i])) {
|
|
144
147
|
// Parse supported AUTH methods
|
|
145
148
|
const parse = /^AUTH (.+)$/.exec(response[i])
|
package/plugins/block_me.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// mail_from.blocklist plugin for this to work fully.
|
|
5
5
|
|
|
6
6
|
const fs = require('node:fs')
|
|
7
|
+
const path = require('node:path')
|
|
7
8
|
const utils = require('haraka-utils')
|
|
8
9
|
|
|
9
10
|
exports.hook_data = (next, connection) => {
|
|
@@ -24,12 +25,12 @@ exports.hook_data_post = function (next, connection) {
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
// Check recipient is the right one
|
|
27
|
-
if (connection.transaction.rcpt_to[0].address
|
|
28
|
+
if (connection.transaction.rcpt_to[0].address.toLowerCase() != recip) {
|
|
28
29
|
return next()
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
// Check sender is in list
|
|
32
|
-
const sender = connection.transaction.mail_from.address
|
|
33
|
+
const sender = connection.transaction.mail_from.address
|
|
33
34
|
if (!utils.in_array(sender, senders)) {
|
|
34
35
|
return next(DENY, `You are not allowed to block mail, ${sender}`)
|
|
35
36
|
}
|
|
@@ -45,8 +46,9 @@ exports.hook_data_post = function (next, connection) {
|
|
|
45
46
|
|
|
46
47
|
connection.transaction.notes.block_me = 1
|
|
47
48
|
|
|
48
|
-
// add to mail_from.blocklist
|
|
49
|
-
|
|
49
|
+
// add to mail_from.blocklist, in the same config dir the plugin reads from
|
|
50
|
+
const blocklist = path.join(this.config.root_path, 'mail_from.blocklist')
|
|
51
|
+
fs.open(blocklist, 'a', (err, fd) => {
|
|
50
52
|
if (err) {
|
|
51
53
|
connection.logerror(this, `Unable to append to mail_from.blocklist: ${err}`)
|
|
52
54
|
return
|
|
@@ -22,9 +22,11 @@ exports.hook_data_post = (next, connection) => {
|
|
|
22
22
|
let user = connection.notes.auth_user
|
|
23
23
|
let domain
|
|
24
24
|
const idx = user.indexOf('@')
|
|
25
|
-
if (idx) {
|
|
25
|
+
if (idx > 0) {
|
|
26
26
|
// If the username is qualified (e.g. user@domain.com)
|
|
27
27
|
// then we make the @domain.com part optional in the regexp.
|
|
28
|
+
// (idx === -1 is "no @"; idx === 0 is a leading @ — neither is
|
|
29
|
+
// a qualified user@domain, so don't split.)
|
|
28
30
|
domain = user.substring(idx)
|
|
29
31
|
user = user.substring(0, idx)
|
|
30
32
|
}
|
package/plugins/process_title.js
CHANGED
|
@@ -64,17 +64,17 @@ exports.hook_init_master = function (next, server) {
|
|
|
64
64
|
server.notes.pt_connections++
|
|
65
65
|
server.notes.pt_concurrent_cluster[msg.wid]++
|
|
66
66
|
count = 0
|
|
67
|
-
Object.keys(server.notes.pt_concurrent_cluster)
|
|
67
|
+
for (const id of Object.keys(server.notes.pt_concurrent_cluster)) {
|
|
68
68
|
count += server.notes.pt_concurrent_cluster[id]
|
|
69
|
-
}
|
|
69
|
+
}
|
|
70
70
|
server.notes.pt_concurrent = count
|
|
71
71
|
break
|
|
72
72
|
case 'process_title.disconnect':
|
|
73
73
|
server.notes.pt_concurrent_cluster[msg.wid]--
|
|
74
74
|
count = 0
|
|
75
|
-
Object.keys(server.notes.pt_concurrent_cluster)
|
|
75
|
+
for (const id of Object.keys(server.notes.pt_concurrent_cluster)) {
|
|
76
76
|
count += server.notes.pt_concurrent_cluster[id]
|
|
77
|
-
}
|
|
77
|
+
}
|
|
78
78
|
server.notes.pt_concurrent = count
|
|
79
79
|
break
|
|
80
80
|
case 'process_title.recipient':
|
|
@@ -109,9 +109,9 @@ exports.hook_init_master = function (next, server) {
|
|
|
109
109
|
delete server.notes.pt_concurrent_cluster[worker.id]
|
|
110
110
|
// Update concurrency
|
|
111
111
|
let count = 0
|
|
112
|
-
Object.keys(server.notes.pt_concurrent_cluster)
|
|
112
|
+
for (const id of Object.keys(server.notes.pt_concurrent_cluster)) {
|
|
113
113
|
count += server.notes.pt_concurrent_cluster[id]
|
|
114
|
-
}
|
|
114
|
+
}
|
|
115
115
|
server.notes.pt_concurrent = count
|
|
116
116
|
server.notes.pt_child_exits++
|
|
117
117
|
})
|
|
@@ -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) {
|
|
@@ -61,7 +69,7 @@ exports.get_config = function (conn) {
|
|
|
61
69
|
if (this.cfg.main.domain_selector === 'mail_from') {
|
|
62
70
|
if (!conn.transaction.mail_from) return this.cfg.main
|
|
63
71
|
dom = conn.transaction.mail_from.host
|
|
64
|
-
address = conn.transaction.mail_from.address
|
|
72
|
+
address = conn.transaction.mail_from.address
|
|
65
73
|
} else {
|
|
66
74
|
if (!conn.transaction.rcpt_to[0]) return this.cfg.main
|
|
67
75
|
dom = conn.transaction.rcpt_to[0].host
|
|
@@ -84,7 +92,7 @@ exports.check_sender = function (next, connection, params) {
|
|
|
84
92
|
const txn = connection?.transaction
|
|
85
93
|
if (!txn) return
|
|
86
94
|
|
|
87
|
-
const email = params[0].address
|
|
95
|
+
const email = params[0].address
|
|
88
96
|
if (!email) {
|
|
89
97
|
txn.results.add(this, { skip: 'mail_from.null', emit: true })
|
|
90
98
|
return next()
|
|
@@ -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) => {
|
|
@@ -26,7 +26,7 @@ exports.hook_mail = function (next, connection, params) {
|
|
|
26
26
|
const txn = connection?.transaction
|
|
27
27
|
if (!txn) return
|
|
28
28
|
|
|
29
|
-
const email = params[0].address
|
|
29
|
+
const email = params[0].address
|
|
30
30
|
if (!email) {
|
|
31
31
|
txn.results.add(this, { skip: 'mail_from.null', emit: true })
|
|
32
32
|
return next()
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
exports.hook_rcpt = (next, connection, params) => {
|
|
6
6
|
if (connection?.transaction) {
|
|
7
|
-
connection.transaction.add_header('X-Envelope-To', params[0].address
|
|
7
|
+
connection.transaction.add_header('X-Envelope-To', params[0].address)
|
|
8
8
|
}
|
|
9
9
|
next()
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
exports.hook_mail = (next, connection, params) => {
|
|
13
13
|
if (connection?.transaction) {
|
|
14
|
-
connection.transaction.add_header('X-Envelope-From', params[0].address
|
|
14
|
+
connection.transaction.add_header('X-Envelope-From', params[0].address)
|
|
15
15
|
}
|
|
16
16
|
next()
|
|
17
17
|
}
|
package/plugins/status.js
CHANGED
|
@@ -171,7 +171,8 @@ exports.hook_init_master = function (next) {
|
|
|
171
171
|
if (msg.event !== 'status.request') return
|
|
172
172
|
|
|
173
173
|
plugin.call_workers(msg, (response) => {
|
|
174
|
-
|
|
174
|
+
const valid = response.filter((el) => el != null)
|
|
175
|
+
msg.result = plugin.merge_worker_responses(msg.params, valid)
|
|
175
176
|
msg.event = 'status.result'
|
|
176
177
|
sender.send(msg)
|
|
177
178
|
})
|
|
@@ -212,13 +213,41 @@ exports.call_master = (cmd, cb) => {
|
|
|
212
213
|
|
|
213
214
|
exports.call_workers = function (cmd, cb) {
|
|
214
215
|
Promise.allSettled(Object.values(server.cluster.workers).map((w) => this.call_worker(w, cmd))).then((r) => {
|
|
215
|
-
cb(
|
|
216
|
-
// r.filter(s => s.status === 'rejected').flatMap(s => s.reason),
|
|
217
|
-
r.filter((s) => s.status === 'fulfilled').flatMap((s) => s.value),
|
|
218
|
-
)
|
|
216
|
+
cb(r.filter((s) => s.status === 'fulfilled').map((s) => s.value))
|
|
219
217
|
})
|
|
220
218
|
}
|
|
221
219
|
|
|
220
|
+
// Merge per-worker responses into a single result matching non-cluster output shape.
|
|
221
|
+
exports.merge_worker_responses = (params, results) => {
|
|
222
|
+
const cmd = params.trim().split(/\s+/).slice(0, 2).join(' ').toUpperCase()
|
|
223
|
+
|
|
224
|
+
switch (cmd) {
|
|
225
|
+
case 'POOL LIST': {
|
|
226
|
+
return Object.assign({}, ...results)
|
|
227
|
+
}
|
|
228
|
+
case 'QUEUE INSPECT': {
|
|
229
|
+
const merged = { delivery_queue: [], temp_fail_queue: [] }
|
|
230
|
+
for (const r of results) {
|
|
231
|
+
if (!r) continue
|
|
232
|
+
if (Array.isArray(r.delivery_queue)) merged.delivery_queue.push(...r.delivery_queue)
|
|
233
|
+
if (Array.isArray(r.temp_fail_queue)) merged.temp_fail_queue.push(...r.temp_fail_queue)
|
|
234
|
+
}
|
|
235
|
+
return merged
|
|
236
|
+
}
|
|
237
|
+
case 'QUEUE STATS': {
|
|
238
|
+
const totals = [0, 0, 0]
|
|
239
|
+
for (const r of results) {
|
|
240
|
+
if (!r) continue
|
|
241
|
+
const parts = String(r).split('/')
|
|
242
|
+
for (let i = 0; i < 3; i++) totals[i] += parseInt(parts[i] ?? 0, 10)
|
|
243
|
+
}
|
|
244
|
+
return totals.join('/')
|
|
245
|
+
}
|
|
246
|
+
default:
|
|
247
|
+
return results
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
222
251
|
// sends command to worker and then wait for response or timeout
|
|
223
252
|
exports.call_worker = (worker, cmd) => {
|
|
224
253
|
return new Promise((resolve) => {
|
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) {
|