Haraka 3.1.5 → 3.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierignore +1 -1
- package/{Changes.md → CHANGELOG.md} +54 -3
- package/CONTRIBUTORS.md +26 -26
- package/Plugins.md +99 -99
- package/README.md +68 -93
- package/SECURITY.md +178 -0
- package/bin/haraka +7 -14
- package/config/plugins +0 -3
- package/config/smtp_forward.ini +10 -0
- package/config/smtp_proxy.ini +10 -0
- package/connection.js +25 -8
- package/docs/Connection.md +126 -39
- package/docs/CoreConfig.md +92 -74
- package/docs/HAProxy.md +41 -25
- package/docs/Logging.md +68 -38
- package/docs/Outbound.md +124 -179
- package/docs/Plugins.md +38 -59
- package/docs/Transaction.md +78 -83
- package/docs/Tutorial.md +122 -209
- package/docs/plugins/aliases.md +1 -141
- package/docs/plugins/auth/auth_ldap.md +2 -39
- package/docs/plugins/max_unrecognized_commands.md +4 -18
- package/docs/plugins/process_title.md +3 -3
- package/docs/plugins/queue/smtp_forward.md +19 -3
- package/docs/plugins/queue/smtp_proxy.md +10 -2
- package/docs/plugins/reseed_rng.md +11 -13
- package/docs/plugins/tls.md +7 -7
- package/docs/plugins/toobusy.md +10 -4
- package/docs/tutorials/SettingUpOutbound.md +40 -48
- package/endpoint.js +32 -2
- package/haraka.js +1 -1
- package/outbound/hmail.js +42 -41
- package/outbound/index.js +7 -4
- package/outbound/tls.js +2 -43
- package/package.json +51 -61
- package/plugins/auth/auth_base.js +9 -3
- package/plugins/auth/auth_proxy.js +14 -11
- package/plugins/block_me.js +4 -2
- package/plugins/prevent_credential_leaks.js +3 -1
- package/plugins/process_title.js +6 -6
- package/plugins/queue/qmail-queue.js +15 -19
- package/plugins/queue/smtp_forward.js +12 -4
- package/plugins/queue/smtp_proxy.js +14 -3
- package/plugins/tls.js +13 -5
- package/plugins/xclient.js +3 -1
- package/server.js +22 -10
- 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 +258 -0
- package/test/endpoint.js +27 -0
- package/test/outbound/bounce_net_errors.js +3 -2
- package/test/outbound/hmail.js +19 -0
- package/test/outbound/index.js +189 -0
- package/test/outbound/queue.js +92 -0
- package/test/plugins/auth/auth_bridge.js +80 -0
- package/test/plugins/auth/flat_file.js +128 -0
- package/test/plugins/block_me.js +157 -0
- package/test/plugins/data.signatures.js +114 -0
- package/test/plugins/delay_deny.js +263 -0
- package/test/plugins/prevent_credential_leaks.js +178 -0
- package/test/plugins/process_title.js +135 -0
- package/test/plugins/queue/deliver.js +99 -0
- package/test/plugins/queue/discard.js +79 -0
- package/test/plugins/queue/lmtp.js +138 -0
- package/test/plugins/queue/qmail-queue.js +99 -0
- package/test/plugins/queue/quarantine.js +81 -0
- package/test/plugins/queue/smtp_bridge.js +154 -0
- package/test/plugins/queue/smtp_forward.js +42 -6
- package/test/plugins/queue/smtp_proxy.js +139 -0
- package/test/plugins/reseed_rng.js +34 -0
- package/test/plugins/tarpit.js +91 -0
- package/test/plugins/tls.js +25 -0
- package/test/plugins/toobusy.js +21 -0
- package/test/plugins/xclient.js +14 -0
- package/test/server.js +231 -0
- package/test/smtp_client.js +45 -12
- package/test/tls_socket.js +220 -0
- package/tls_socket.js +52 -2
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"server",
|
|
10
10
|
"email"
|
|
11
11
|
],
|
|
12
|
-
"version": "3.1.
|
|
12
|
+
"version": "3.1.7",
|
|
13
13
|
"homepage": "http://haraka.github.io",
|
|
14
14
|
"repository": {
|
|
15
15
|
"type": "git",
|
|
@@ -20,69 +20,58 @@
|
|
|
20
20
|
"node": ">=20"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"address-rfc2821": "
|
|
24
|
-
"address-rfc2822": "
|
|
25
|
-
"
|
|
26
|
-
"haraka-
|
|
27
|
-
"haraka-
|
|
28
|
-
"haraka-
|
|
29
|
-
"haraka-
|
|
30
|
-
"haraka-
|
|
31
|
-
"haraka-
|
|
32
|
-
"haraka-
|
|
33
|
-
"haraka-
|
|
34
|
-
"haraka-
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"npid": "~0.4.0",
|
|
41
|
-
"redis": "~5.11.0",
|
|
42
|
-
"semver": "^7.7.4",
|
|
43
|
-
"sockaddr": "^1.0.1",
|
|
44
|
-
"sprintf-js": "~1.1.3"
|
|
23
|
+
"address-rfc2821": "~2.2.0",
|
|
24
|
+
"address-rfc2822": "~2.2.3",
|
|
25
|
+
"haraka-config": "~1.6.0",
|
|
26
|
+
"haraka-constants": "~1.0.7",
|
|
27
|
+
"haraka-dsn": "~1.2.0",
|
|
28
|
+
"haraka-email-message": "~1.3.3",
|
|
29
|
+
"haraka-net-utils": "~1.8.2",
|
|
30
|
+
"haraka-notes": "~1.1.3",
|
|
31
|
+
"haraka-plugin-redis": "~2.0.11",
|
|
32
|
+
"haraka-results": "~2.3.0",
|
|
33
|
+
"haraka-tld": "~1.3.4",
|
|
34
|
+
"haraka-utils": "~1.1.4",
|
|
35
|
+
"ipaddr.js": "~2.4.0",
|
|
36
|
+
"node-gyp": "~12.3.0",
|
|
37
|
+
"nopt": "~10.0.0",
|
|
38
|
+
"redis": "~5.12.1",
|
|
39
|
+
"semver": "~7.8.0"
|
|
45
40
|
},
|
|
46
41
|
"optionalDependencies": {
|
|
47
|
-
"haraka
|
|
48
|
-
"haraka-plugin-
|
|
49
|
-
"haraka-plugin-
|
|
50
|
-
"haraka-plugin-
|
|
51
|
-
"haraka-plugin-
|
|
52
|
-
"haraka-plugin-bounce": "
|
|
53
|
-
"haraka-plugin-clamd": "
|
|
54
|
-
"haraka-plugin-dcc": "
|
|
55
|
-
"haraka-plugin-dkim": "
|
|
56
|
-
"haraka-plugin-dns-list": "
|
|
57
|
-
"haraka-plugin-early_talker": "
|
|
58
|
-
"haraka-plugin-
|
|
59
|
-
"haraka-plugin-
|
|
60
|
-
"haraka-plugin-
|
|
61
|
-
"haraka-plugin-
|
|
62
|
-
"haraka-plugin-
|
|
63
|
-
"haraka-plugin-
|
|
64
|
-
"haraka-plugin-
|
|
65
|
-
"haraka-plugin-
|
|
66
|
-
"haraka-plugin-
|
|
67
|
-
"haraka-plugin-
|
|
68
|
-
"haraka-plugin-
|
|
69
|
-
"haraka-plugin-
|
|
70
|
-
"haraka-plugin-
|
|
71
|
-
"haraka-plugin-
|
|
72
|
-
"haraka-plugin-
|
|
73
|
-
"haraka-plugin-
|
|
74
|
-
"haraka-plugin-
|
|
75
|
-
"haraka-plugin-spamassassin": "^1.0.3",
|
|
76
|
-
"haraka-plugin-spf": "^1.2.11",
|
|
77
|
-
"haraka-plugin-syslog": "^1.0.7",
|
|
78
|
-
"haraka-plugin-uribl": "^1.0.10",
|
|
79
|
-
"haraka-plugin-watch": "^2.0.9",
|
|
80
|
-
"@techteamer/ocsp": "^1.0.1"
|
|
42
|
+
"@haraka/ocsp": "~1.2.0",
|
|
43
|
+
"haraka-plugin-access": "~1.2.0",
|
|
44
|
+
"haraka-plugin-aliases": "~1.0.3",
|
|
45
|
+
"haraka-plugin-asn": "~2.1.0",
|
|
46
|
+
"haraka-plugin-attachment": "~1.2.0",
|
|
47
|
+
"haraka-plugin-bounce": "~2.1.2",
|
|
48
|
+
"haraka-plugin-clamd": "~1.0.2",
|
|
49
|
+
"haraka-plugin-dcc": "~1.0.3",
|
|
50
|
+
"haraka-plugin-dkim": "~1.1.2",
|
|
51
|
+
"haraka-plugin-dns-list": "~1.2.4",
|
|
52
|
+
"haraka-plugin-early_talker": "~1.0.2",
|
|
53
|
+
"haraka-plugin-fcrdns": "~1.1.2",
|
|
54
|
+
"haraka-plugin-geoip": "~1.1.2",
|
|
55
|
+
"haraka-plugin-greylist": "~1.1.1",
|
|
56
|
+
"haraka-plugin-headers": "~1.1.2",
|
|
57
|
+
"haraka-plugin-helo.checks": "~1.1.1",
|
|
58
|
+
"haraka-plugin-karma": "~2.4.1",
|
|
59
|
+
"haraka-plugin-known-senders": "~1.1.4",
|
|
60
|
+
"haraka-plugin-limit": "~1.2.7",
|
|
61
|
+
"haraka-plugin-mail_from.is_resolvable": "~1.2.0",
|
|
62
|
+
"haraka-plugin-messagesniffer": "~1.0.1",
|
|
63
|
+
"haraka-plugin-qmail-deliverable": "~1.3.5",
|
|
64
|
+
"haraka-plugin-relay": "~1.0.2",
|
|
65
|
+
"haraka-plugin-rspamd": "~1.5.0",
|
|
66
|
+
"haraka-plugin-spamassassin": "~1.0.4",
|
|
67
|
+
"haraka-plugin-spf": "~1.2.11",
|
|
68
|
+
"haraka-plugin-syslog": "~1.1.0",
|
|
69
|
+
"haraka-plugin-uribl": "~1.0.10"
|
|
81
70
|
},
|
|
82
71
|
"devDependencies": {
|
|
83
|
-
"@haraka/eslint-config": "
|
|
84
|
-
"haraka-test-fixtures": "
|
|
85
|
-
"mock-require": "
|
|
72
|
+
"@haraka/eslint-config": "~2.0.4",
|
|
73
|
+
"haraka-test-fixtures": "~1.6.0",
|
|
74
|
+
"mock-require": "~3.0.3"
|
|
86
75
|
},
|
|
87
76
|
"bugs": {
|
|
88
77
|
"mail": "haraka.mail@gmail.com",
|
|
@@ -101,7 +90,8 @@
|
|
|
101
90
|
"test": "sh ./run_tests",
|
|
102
91
|
"versions": "npx npm-dep-mgr check",
|
|
103
92
|
"versions:fix": "npx npm-dep-mgr update",
|
|
104
|
-
"test:coverage": "
|
|
93
|
+
"test:coverage": "node --test --test-concurrency=1 --experimental-test-coverage",
|
|
94
|
+
"test:coverage:lcov": "mkdir -p coverage && node --test --test-concurrency=1 --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=coverage/lcov.info"
|
|
105
95
|
},
|
|
106
96
|
"prettier": {
|
|
107
97
|
"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) => {
|
|
@@ -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) {
|
|
@@ -264,7 +272,7 @@ exports.queue_forward = function (next, connection) {
|
|
|
264
272
|
smtp_client.send_command('DATA')
|
|
265
273
|
return
|
|
266
274
|
}
|
|
267
|
-
smtp_client.send_command('RCPT', `TO:${txn.rcpt_to[rcpt].format(!smtp_client.
|
|
275
|
+
smtp_client.send_command('RCPT', `TO:${txn.rcpt_to[rcpt].format(!smtp_client.smtputf8)}`)
|
|
268
276
|
rcpt++
|
|
269
277
|
}
|
|
270
278
|
|
|
@@ -354,10 +362,10 @@ exports.get_mx = function (next, hmail, domain) {
|
|
|
354
362
|
}
|
|
355
363
|
|
|
356
364
|
// apply auth/mx options
|
|
357
|
-
|
|
358
|
-
if (cfg[o] === undefined)
|
|
365
|
+
for (const o of mx_opts) {
|
|
366
|
+
if (cfg[o] === undefined) continue
|
|
359
367
|
mx[o] = this.cfg[dom][o]
|
|
360
|
-
}
|
|
368
|
+
}
|
|
361
369
|
|
|
362
370
|
next(OK, mx)
|
|
363
371
|
}
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// and passes back any errors seen on the ongoing server to the
|
|
5
5
|
// originating server.
|
|
6
6
|
|
|
7
|
-
const smtp_client_mod = require('
|
|
7
|
+
const smtp_client_mod = require('../../smtp_client')
|
|
8
|
+
const tls_socket = require('../../tls_socket')
|
|
8
9
|
|
|
9
10
|
exports.register = function () {
|
|
10
11
|
this.load_smtp_proxy_ini()
|
|
@@ -18,13 +19,23 @@ exports.load_smtp_proxy_ini = function () {
|
|
|
18
19
|
this.cfg = this.config.get(
|
|
19
20
|
'smtp_proxy.ini',
|
|
20
21
|
{
|
|
21
|
-
booleans: [
|
|
22
|
+
booleans: [
|
|
23
|
+
'-main.enable_tls',
|
|
24
|
+
'+main.enable_outbound',
|
|
25
|
+
'+tls.requestCert',
|
|
26
|
+
'+tls.honorCipherOrder',
|
|
27
|
+
'-tls.rejectUnauthorized',
|
|
28
|
+
],
|
|
22
29
|
},
|
|
23
30
|
() => {
|
|
24
31
|
this.load_smtp_proxy_ini()
|
|
25
32
|
},
|
|
26
33
|
)
|
|
27
34
|
|
|
35
|
+
// Build backend TLS options from tls.ini [main] + this plugin's [tls] section.
|
|
36
|
+
// Re-derived on every (re)load so SIGHUP picks up edits.
|
|
37
|
+
this.tls_options = tls_socket.load_plugin_tls_options(this.cfg.tls || {})
|
|
38
|
+
|
|
28
39
|
if (this.cfg.main.enable_outbound) {
|
|
29
40
|
this.lognotice('outbound enabled, will default to disabled in Haraka v3 (see #1472)')
|
|
30
41
|
}
|
|
@@ -82,7 +93,7 @@ exports.hook_rcpt_ok = (next, connection, recipient) => {
|
|
|
82
93
|
return
|
|
83
94
|
}
|
|
84
95
|
smtp_client.next = next
|
|
85
|
-
smtp_client.send_command('RCPT', `TO:${recipient.format(!smtp_client.
|
|
96
|
+
smtp_client.send_command('RCPT', `TO:${recipient.format(!smtp_client.smtputf8)}`)
|
|
86
97
|
}
|
|
87
98
|
|
|
88
99
|
exports.hook_data = (next, connection) => {
|
package/plugins/tls.js
CHANGED
|
@@ -85,6 +85,14 @@ exports.upgrade_connection = function (next, connection, params) {
|
|
|
85
85
|
/* Watch for STARTTLS directive from client. */
|
|
86
86
|
if (params[0].toUpperCase() !== 'STARTTLS') return next()
|
|
87
87
|
|
|
88
|
+
// RFC 3207 §4: discard any plaintext the client pipelined after
|
|
89
|
+
// STARTTLS. Otherwise connection.respond() restores state to CMD and
|
|
90
|
+
// re-enters _process_data(), parsing and executing those buffered
|
|
91
|
+
// cleartext commands before the TLS handshake completes (STARTTLS
|
|
92
|
+
// command injection). Mirrors the buffer-nuke already used for
|
|
93
|
+
// SSL-over-plaintext detection in connection.process_line().
|
|
94
|
+
connection.current_data = null
|
|
95
|
+
|
|
88
96
|
/* Respond to STARTTLS command. */
|
|
89
97
|
connection.respond(220, 'Go ahead.')
|
|
90
98
|
|
|
@@ -109,7 +117,7 @@ exports.upgrade_connection = function (next, connection, params) {
|
|
|
109
117
|
connection.notes.cleanUpDisconnect = nextOnce
|
|
110
118
|
|
|
111
119
|
/* Upgrade the connection to TLS. */
|
|
112
|
-
connection.client.upgrade((verified,
|
|
120
|
+
connection.client.upgrade((verified, verifyError, cert, cipher) => {
|
|
113
121
|
if (called_next) return
|
|
114
122
|
clearTimeout(connection.notes.tls_timer)
|
|
115
123
|
called_next = true
|
|
@@ -117,12 +125,12 @@ exports.upgrade_connection = function (next, connection, params) {
|
|
|
117
125
|
connection.setTLS({
|
|
118
126
|
cipher,
|
|
119
127
|
verified,
|
|
120
|
-
|
|
128
|
+
verifyError,
|
|
121
129
|
peerCertificate: cert,
|
|
122
130
|
})
|
|
123
131
|
|
|
124
132
|
connection.results.add(plugin, connection.tls)
|
|
125
|
-
plugin.emit_upgrade_msg(connection, verified,
|
|
133
|
+
plugin.emit_upgrade_msg(connection, verified, verifyError, cert, cipher)
|
|
126
134
|
next(OK)
|
|
127
135
|
})
|
|
128
136
|
})
|
|
@@ -135,13 +143,13 @@ exports.hook_disconnect = (next, connection) => {
|
|
|
135
143
|
next()
|
|
136
144
|
}
|
|
137
145
|
|
|
138
|
-
exports.emit_upgrade_msg = function (conn, verified,
|
|
146
|
+
exports.emit_upgrade_msg = function (conn, verified, verifyError, cert, cipher) {
|
|
139
147
|
let msg = 'secured:'
|
|
140
148
|
if (cipher) {
|
|
141
149
|
msg += ` cipher=${cipher.name} version=${cipher.version}`
|
|
142
150
|
}
|
|
143
151
|
msg += ` verified=${verified}`
|
|
144
|
-
if (
|
|
152
|
+
if (verifyError) msg += ` error="${verifyError}"`
|
|
145
153
|
if (cert) {
|
|
146
154
|
if (cert.subject) {
|
|
147
155
|
msg += ` cn="${cert.subject.CN}" organization="${cert.subject.O}"`
|
package/plugins/xclient.js
CHANGED
|
@@ -110,7 +110,9 @@ exports.hook_unrecognized_command = function (next, connection, params) {
|
|
|
110
110
|
connection.set('remote.login', xclient.login ? xclient.login : undefined)
|
|
111
111
|
connection.set('hello.host', xclient.helo ? xclient.helo : undefined)
|
|
112
112
|
connection.set('local.ip', xclient.destaddr ? xclient.destaddr : undefined)
|
|
113
|
-
|
|
113
|
+
// parseInt so downstream numeric checks (e.g. the 587/465 auth-required
|
|
114
|
+
// gate in connection.cmd_mail) work; matches the PROXY path.
|
|
115
|
+
connection.set('local.port', xclient.destport ? parseInt(xclient.destport, 10) : undefined)
|
|
114
116
|
if (xclient.proto) {
|
|
115
117
|
connection.set('hello', 'verb', xclient.proto === 'esmtp' ? 'EHLO' : 'HELO')
|
|
116
118
|
}
|
package/server.js
CHANGED
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
// smtp network server
|
|
3
3
|
|
|
4
4
|
const cluster = require('node:cluster')
|
|
5
|
+
const { spawn } = require('node:child_process')
|
|
5
6
|
const fs = require('node:fs')
|
|
6
7
|
const os = require('node:os')
|
|
7
8
|
const path = require('node:path')
|
|
8
9
|
const tls = require('node:tls')
|
|
9
|
-
|
|
10
|
-
const daemon = require('daemon')
|
|
11
10
|
const constants = require('haraka-constants')
|
|
12
11
|
|
|
13
12
|
const tls_socket = require('./tls_socket')
|
|
@@ -77,15 +76,26 @@ Server.daemonize = function () {
|
|
|
77
76
|
// we get a spurious 'Exiting' log entry.
|
|
78
77
|
process.removeAllListeners('exit')
|
|
79
78
|
Server.lognotice('Daemonizing...')
|
|
80
|
-
}
|
|
81
79
|
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
const log_fd = fs.openSync(c.daemon_log_file, 'a')
|
|
81
|
+
const child = spawn(process.execPath, process.argv.slice(1), {
|
|
82
|
+
detached: true,
|
|
83
|
+
stdio: ['ignore', log_fd, log_fd],
|
|
84
|
+
env: { ...process.env, __daemon: '1' },
|
|
85
|
+
cwd: process.cwd(),
|
|
86
|
+
})
|
|
87
|
+
child.unref()
|
|
88
|
+
process.exit(0)
|
|
89
|
+
}
|
|
84
90
|
|
|
85
91
|
// We are the daemon from here on...
|
|
86
|
-
const npid = require('npid')
|
|
87
92
|
try {
|
|
88
|
-
|
|
93
|
+
fs.writeFileSync(c.daemon_pid_file, `${process.pid}\n`, { flag: 'wx' })
|
|
94
|
+
process.on('exit', () => {
|
|
95
|
+
try {
|
|
96
|
+
fs.unlinkSync(c.daemon_pid_file)
|
|
97
|
+
} catch {}
|
|
98
|
+
})
|
|
89
99
|
} catch (err) {
|
|
90
100
|
Server.logerror(err.message)
|
|
91
101
|
logger.dump_and_exit(1)
|
|
@@ -165,7 +175,7 @@ Server._graceful = async (shutdown) => {
|
|
|
165
175
|
const todo = []
|
|
166
176
|
|
|
167
177
|
for (const id of Object.keys(cluster.workers)) {
|
|
168
|
-
todo.push((
|
|
178
|
+
todo.push(() => {
|
|
169
179
|
return new Promise((resolve) => {
|
|
170
180
|
Server.lognotice(`Killing worker: ${id}`)
|
|
171
181
|
const worker = cluster.workers[id]
|
|
@@ -213,8 +223,10 @@ Server._graceful = async (shutdown) => {
|
|
|
213
223
|
}
|
|
214
224
|
|
|
215
225
|
while (todo.length) {
|
|
216
|
-
// process batches of workers
|
|
217
|
-
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()))
|
|
218
230
|
}
|
|
219
231
|
|
|
220
232
|
if (shutdown) {
|