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
|
@@ -44,8 +44,7 @@ Configuration is stored in smtp_proxy.ini in the following keys:
|
|
|
44
44
|
|
|
45
45
|
- enable_tls=[true|yes|1]
|
|
46
46
|
|
|
47
|
-
Enable TLS with the forward host (if
|
|
48
|
-
the tls plugin.
|
|
47
|
+
Enable opportunistic TLS with the forward host via `STARTTLS` (if the host advertises it).
|
|
49
48
|
|
|
50
49
|
- auth_type=[plain|login]
|
|
51
50
|
|
|
@@ -58,3 +57,12 @@ Configuration is stored in smtp_proxy.ini in the following keys:
|
|
|
58
57
|
- auth_pass=PASSWORD
|
|
59
58
|
|
|
60
59
|
SMTP AUTH password to use.
|
|
60
|
+
|
|
61
|
+
- [tls]
|
|
62
|
+
|
|
63
|
+
Client STARTTLS options are assembled by merging:
|
|
64
|
+
|
|
65
|
+
1. `tls.ini` `[main]` — the global Haraka TLS config.
|
|
66
|
+
2. `smtp_proxy.ini` `[tls]` — overrides. Anything set here wins.
|
|
67
|
+
|
|
68
|
+
Changes to `tls.ini` require a Haraka restart to apply to the proxy path; changes to `smtp_proxy.ini` are picked up by the existing reload hook.
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
# reseed_rng
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
Reseeds `Math.random()` in each cluster worker at start-up using
|
|
4
|
+
`crypto.randomBytes(256)`. Without this, workers forked at nearly the
|
|
5
|
+
same time can end up with correlated PRNG state, which can produce
|
|
6
|
+
UUID collisions and other "this should be impossible" bugs.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
problems like UUID collisions. When using the 'cluster' module, it's
|
|
11
|
-
quite easy to observe this behavior.
|
|
8
|
+
The plugin relies on [seedrandom](https://www.npmjs.com/package/seedrandom)
|
|
9
|
+
being loaded so that `Math.seedrandom()` is available.
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
to reseed the RNG with a sligtly better seed at spawned-process startup
|
|
16
|
-
time.
|
|
11
|
+
Anyone running with `nodes=...` in `smtp.ini` (i.e. cluster mode) should
|
|
12
|
+
consider enabling this plugin.
|
|
17
13
|
|
|
18
|
-
|
|
14
|
+
## Configuration
|
|
15
|
+
|
|
16
|
+
No configuration.
|
package/docs/plugins/tls.md
CHANGED
|
@@ -155,9 +155,9 @@ Specifies minimum allowable TLS protocol version to use. Example:
|
|
|
155
155
|
|
|
156
156
|
minVersion=TLSv1.1
|
|
157
157
|
|
|
158
|
-
If unset, the default is
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
If unset, the default is Node's `tls.DEFAULT_MIN_VERSION` constant
|
|
159
|
+
(currently `'TLSv1.2'`). Valid values: `'TLSv1.3'`, `'TLSv1.2'`,
|
|
160
|
+
`'TLSv1.1'`, `'TLSv1'`.
|
|
161
161
|
|
|
162
162
|
### honorCipherOrder
|
|
163
163
|
|
|
@@ -195,10 +195,10 @@ requireAuthorized[]=465
|
|
|
195
195
|
|
|
196
196
|
### secureProtocol
|
|
197
197
|
|
|
198
|
-
Specifies the OpenSSL API function used
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
198
|
+
Legacy. Specifies the OpenSSL API function used to negotiate TLS — see
|
|
199
|
+
the [OpenSSL API page](https://www.openssl.org/docs/manmaster/ssl/ssl.html).
|
|
200
|
+
Prefer `minVersion` for modern setups; `secureProtocol` is only useful
|
|
201
|
+
to lock to a specific historic protocol.
|
|
202
202
|
|
|
203
203
|
### requestOCSP
|
|
204
204
|
|
package/docs/plugins/toobusy.md
CHANGED
|
@@ -5,11 +5,17 @@ latency is too high.
|
|
|
5
5
|
|
|
6
6
|
See https://github.com/STRML/node-toobusy for details.
|
|
7
7
|
|
|
8
|
-
To use this plugin you
|
|
9
|
-
|
|
8
|
+
To use this plugin you must install the [`toobusy-js`](https://www.npmjs.com/package/toobusy-js)
|
|
9
|
+
module — it is not bundled with Haraka. From your Haraka install
|
|
10
|
+
directory:
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
```sh
|
|
13
|
+
npm install toobusy-js
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
This plugin registers on the `connect` hook with priority `-100`, so it
|
|
17
|
+
runs ahead of other `connect`/`lookup_rdns` plugins. Listing it near the
|
|
18
|
+
top of `config/plugins` is still a good idea for clarity.
|
|
13
19
|
|
|
14
20
|
## Configuration
|
|
15
21
|
|
|
@@ -1,70 +1,62 @@
|
|
|
1
1
|
# Configuring Haraka For Outbound Email
|
|
2
2
|
|
|
3
|
-
It is
|
|
4
|
-
first there are external things you may want to sort out:
|
|
3
|
+
It is straightforward to run Haraka as an outbound (submission) mail server. Before turning on the server itself, get a few external things in order:
|
|
5
4
|
|
|
6
|
-
-
|
|
7
|
-
|
|
8
|
-
-
|
|
9
|
-
people seem to think it helps.
|
|
5
|
+
- **DNS PTR record** — make sure it matches the A/AAAA record of the host you are sending from. Receivers that disagree on `HELO`/PTR will treat your mail with suspicion.
|
|
6
|
+
- **SPF, DKIM, and DMARC** — publish records for any domain you send from. Most receivers downgrade or reject mail without them. Haraka signs outbound with [haraka-plugin-dkim](https://github.com/haraka/haraka-plugin-dkim).
|
|
7
|
+
- **Reverse DNS at the IP owner** — if your hosting provider controls the PTR, set the value through their console.
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
things working, and they are specific to your network and your DNS hosting.
|
|
9
|
+
How to provision DNS varies by provider; the records are network-specific so no one-size-fits-all command applies.
|
|
13
10
|
|
|
14
|
-
##
|
|
11
|
+
## Background
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
term the internals use. The process is simple - if a plugin in Haraka tells
|
|
18
|
-
the internals that this mail is to be relayed, then it gets queued in the
|
|
19
|
-
"queue" directory for delivery. Then it will go through several delivery
|
|
20
|
-
attempts until it is either successful or fails hard for some reason. A
|
|
21
|
-
hard failure will result in a bounce email being sent to the "MAIL FROM"
|
|
22
|
-
address used when connecting to Haraka. If that address also bounces then
|
|
23
|
-
it is considered a "double bounce" and Haraka will log an error and drop it
|
|
24
|
-
on the floor.
|
|
13
|
+
Haraka treats outbound mail as "relaying". When any plugin sets `connection.relaying = true`, the message is queued for outbound delivery once `DATA` ends. The outbound engine then tries each MX in sequence; on permanent failure a DSN is generated and sent to the `MAIL FROM` address. If the DSN itself bounces, Haraka logs the "double bounce" and drops it.
|
|
25
14
|
|
|
26
|
-
##
|
|
15
|
+
## Setup
|
|
27
16
|
|
|
28
|
-
|
|
29
|
-
is slightly different from the "old" model where there would simply be a
|
|
30
|
-
check based on the connecting IP address to see if it was valid to relay.
|
|
31
|
-
Note however that Haraka doesn't stop you doing it this way - we just don't
|
|
32
|
-
provide a plugin to do that by default - you will have to write one. The
|
|
33
|
-
reason is purely based on security and personal preference.
|
|
17
|
+
Modern submission uses **implicit TLS on port 465** (RFC 8314); port 587 with `STARTTLS` is also still common. Plain port 25 is for server-to-server traffic and should not be used for submission.
|
|
34
18
|
|
|
35
|
-
|
|
19
|
+
Create a new Haraka instance:
|
|
36
20
|
|
|
37
|
-
|
|
38
|
-
|
|
21
|
+
```sh
|
|
22
|
+
haraka -i haraka-outbound
|
|
23
|
+
cd haraka-outbound
|
|
24
|
+
```
|
|
39
25
|
|
|
40
|
-
|
|
26
|
+
In `config/smtp.ini`, set the listener:
|
|
41
27
|
|
|
42
|
-
|
|
28
|
+
```ini
|
|
29
|
+
listen=[::0]:465,[::0]:587
|
|
30
|
+
smtps_port=465
|
|
31
|
+
```
|
|
43
32
|
|
|
44
|
-
|
|
45
|
-
auth/flat_file" > config/plugins
|
|
33
|
+
Anything in `smtps_port` runs implicit TLS; the other ports advertise `STARTTLS`.
|
|
46
34
|
|
|
47
|
-
|
|
48
|
-
and password:
|
|
35
|
+
Enable just the TLS and auth plugins. AUTH is only advertised after TLS is established (except for connections from localhost):
|
|
49
36
|
|
|
50
|
-
|
|
37
|
+
```sh
|
|
38
|
+
cat > config/plugins <<'EOF'
|
|
39
|
+
tls
|
|
40
|
+
auth/flat_file
|
|
41
|
+
EOF
|
|
42
|
+
```
|
|
51
43
|
|
|
52
|
-
|
|
53
|
-
what can go in that file.
|
|
44
|
+
Add a user to `config/auth_flat_file.ini`. See [`docs/plugins/auth/flat_file.md`](../plugins/auth/flat_file.md) for the format.
|
|
54
45
|
|
|
55
|
-
|
|
46
|
+
Start Haraka:
|
|
56
47
|
|
|
57
|
-
|
|
48
|
+
```sh
|
|
49
|
+
haraka -c .
|
|
50
|
+
```
|
|
58
51
|
|
|
59
|
-
|
|
60
|
-
an email address you can monitor in place of youremail@yourdomain.com, and the
|
|
61
|
-
username and password you added for the --auth-user and --auth-password params:
|
|
52
|
+
In another shell, test with [swaks](https://www.jetmore.org/john/code/swaks/) — substitute your real test address and the credentials you configured:
|
|
62
53
|
|
|
63
|
-
|
|
64
|
-
|
|
54
|
+
```sh
|
|
55
|
+
swaks --to youremail@yourdomain.com --from test@example.com \
|
|
56
|
+
--server localhost --port 587 --tls \
|
|
57
|
+
--auth-user testuser --auth-password testpassword
|
|
58
|
+
```
|
|
65
59
|
|
|
66
|
-
|
|
67
|
-
the recipient email address (easiest to make this your webmail account) and
|
|
68
|
-
see that the email arrived.
|
|
60
|
+
For port 465 (implicit TLS), use `--tls-on-connect` instead of `--tls`.
|
|
69
61
|
|
|
70
|
-
|
|
62
|
+
Watch the swaks output for errors and confirm the message arrives. That's all the basic configuration you need; once you're satisfied, turn on DKIM signing for the domains you send from.
|
package/endpoint.js
CHANGED
|
@@ -2,12 +2,42 @@
|
|
|
2
2
|
// Socket address parser/formatter and server binding helper
|
|
3
3
|
|
|
4
4
|
const fs = require('node:fs/promises')
|
|
5
|
-
const
|
|
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
|
+
}
|
|
6
36
|
|
|
7
37
|
module.exports = function endpoint(addr, defaultPort) {
|
|
8
38
|
try {
|
|
9
39
|
if ('string' === typeof addr || 'number' === typeof addr) {
|
|
10
|
-
addr =
|
|
40
|
+
addr = parseSockaddr(addr, defaultPort)
|
|
11
41
|
const match = /^(.*):([0-7]{3})$/.exec(addr.path || '')
|
|
12
42
|
if (match) {
|
|
13
43
|
addr.path = match[1]
|
package/haraka.js
CHANGED
|
@@ -25,7 +25,7 @@ exports.version = utils.getVersion(__dirname)
|
|
|
25
25
|
|
|
26
26
|
process.on('uncaughtException', (err) => {
|
|
27
27
|
if (err.stack) {
|
|
28
|
-
err.stack.split('\n')
|
|
28
|
+
for (const line of err.stack.split('\n')) logger.crit(line)
|
|
29
29
|
} else {
|
|
30
30
|
logger.crit(`Caught exception: ${JSON.stringify(err)}`)
|
|
31
31
|
}
|
package/outbound/hmail.js
CHANGED
|
@@ -87,9 +87,10 @@ class HMailItem extends events.EventEmitter {
|
|
|
87
87
|
this.file_size = stats.size
|
|
88
88
|
this.read_todo()
|
|
89
89
|
} catch (err) {
|
|
90
|
-
//
|
|
90
|
+
// The file is unreadable (deleted, permissions, I/O error) and this.todo
|
|
91
|
+
// is still null, so there is no sender to bounce to. Release the queue slot.
|
|
91
92
|
this.logerror(`Error obtaining file size: ${err}`)
|
|
92
|
-
this.
|
|
93
|
+
this.next_cb()
|
|
93
94
|
}
|
|
94
95
|
}
|
|
95
96
|
|
|
@@ -265,12 +266,12 @@ class HMailItem extends events.EventEmitter {
|
|
|
265
266
|
}
|
|
266
267
|
|
|
267
268
|
async found_mx(mxs) {
|
|
268
|
-
// support
|
|
269
|
+
// support RFC 7505 null MX
|
|
269
270
|
if (mxs.length === 1 && mxs[0].priority === 0 && mxs[0].exchange === '') {
|
|
270
271
|
for (const rcpt of this.todo.rcpt_to) {
|
|
271
272
|
this.extend_rcpt_with_dsn(
|
|
272
273
|
rcpt,
|
|
273
|
-
DSN.
|
|
274
|
+
DSN.addr_null_mx(`Domain ${this.todo.domain} sends and receives no email (NULL MX)`),
|
|
274
275
|
)
|
|
275
276
|
}
|
|
276
277
|
return this.bounce(`Domain ${this.todo.domain} sends and receives no email (NULL MX)`)
|
|
@@ -458,7 +459,7 @@ class HMailItem extends events.EventEmitter {
|
|
|
458
459
|
} else if (r.toUpperCase() === 'ENHANCEDSTATUSCODES') {
|
|
459
460
|
smtp_properties.enh_status_codes = true
|
|
460
461
|
} else if (r.toUpperCase() === 'SMTPUTF8') {
|
|
461
|
-
smtp_properties.
|
|
462
|
+
smtp_properties.smtputf8 = true
|
|
462
463
|
} else {
|
|
463
464
|
// Check for SIZE parameter and limit
|
|
464
465
|
let matches = r.match(/^SIZE\s+(\d+)$/)
|
|
@@ -475,9 +476,9 @@ class HMailItem extends events.EventEmitter {
|
|
|
475
476
|
}
|
|
476
477
|
|
|
477
478
|
function get_reverse_path_with_params() {
|
|
478
|
-
const rp = self.todo.mail_from.format(!smtp_properties.
|
|
479
|
+
const rp = self.todo.mail_from.format(!smtp_properties.smtputf8)
|
|
479
480
|
let rp_params = ''
|
|
480
|
-
if (smtp_properties.
|
|
481
|
+
if (smtp_properties.smtputf8 && has_non_ascii(rp)) rp_params += ' SMTPUTF8'
|
|
481
482
|
return `FROM:${rp}${rp_params}`
|
|
482
483
|
}
|
|
483
484
|
|
|
@@ -677,12 +678,12 @@ class HMailItem extends events.EventEmitter {
|
|
|
677
678
|
processing_mail = false
|
|
678
679
|
// Release back to the pool and instruct it to terminate this connection
|
|
679
680
|
client_pool.release_client(socket, mx)
|
|
680
|
-
self.todo.rcpt_to
|
|
681
|
+
for (const rcpt of self.todo.rcpt_to) {
|
|
681
682
|
self.extend_rcpt_with_dsn(
|
|
682
683
|
rcpt,
|
|
683
684
|
DSN.proto_invalid_command(`Unrecognized response from upstream server: ${line}`),
|
|
684
685
|
)
|
|
685
|
-
}
|
|
686
|
+
}
|
|
686
687
|
self.bounce(`Unrecognized response from upstream server: ${line}`, { mx })
|
|
687
688
|
return
|
|
688
689
|
}
|
|
@@ -719,14 +720,14 @@ class HMailItem extends events.EventEmitter {
|
|
|
719
720
|
}
|
|
720
721
|
// Error
|
|
721
722
|
reason = response.join(' ')
|
|
722
|
-
|
|
723
|
+
for (const rcpt of recipients) {
|
|
723
724
|
rcpt.dsn_action = 'delayed'
|
|
724
725
|
rcpt.dsn_smtp_code = code
|
|
725
726
|
rcpt.dsn_smtp_extc = extc
|
|
726
727
|
rcpt.dsn_status = extc
|
|
727
728
|
rcpt.dsn_smtp_response = response.join(' ')
|
|
728
729
|
rcpt.dsn_remote_mta = mx.exchange
|
|
729
|
-
}
|
|
730
|
+
}
|
|
730
731
|
send_command('QUIT')
|
|
731
732
|
processing_mail = false
|
|
732
733
|
return self.temp_fail(`Upstream error: ${code} ${extc ? `${extc} ` : ''}${reason}`)
|
|
@@ -755,14 +756,14 @@ class HMailItem extends events.EventEmitter {
|
|
|
755
756
|
}
|
|
756
757
|
} else if (processing_mail) {
|
|
757
758
|
reason = response.join(' ')
|
|
758
|
-
|
|
759
|
+
for (const rcpt of recipients) {
|
|
759
760
|
rcpt.dsn_action = 'delayed'
|
|
760
761
|
rcpt.dsn_smtp_code = code
|
|
761
762
|
rcpt.dsn_smtp_extc = extc
|
|
762
763
|
rcpt.dsn_status = extc
|
|
763
764
|
rcpt.dsn_smtp_response = response.join(' ')
|
|
764
765
|
rcpt.dsn_remote_mta = mx.exchange
|
|
765
|
-
}
|
|
766
|
+
}
|
|
766
767
|
send_command('QUIT')
|
|
767
768
|
processing_mail = false
|
|
768
769
|
return self.temp_fail(`Upstream error: ${code} ${extc ? `${extc} ` : ''}${reason}`)
|
|
@@ -802,14 +803,14 @@ class HMailItem extends events.EventEmitter {
|
|
|
802
803
|
}
|
|
803
804
|
}
|
|
804
805
|
} else {
|
|
805
|
-
|
|
806
|
+
for (const rcpt of recipients) {
|
|
806
807
|
rcpt.dsn_action = 'failed'
|
|
807
808
|
rcpt.dsn_smtp_code = code
|
|
808
809
|
rcpt.dsn_smtp_extc = extc
|
|
809
810
|
rcpt.dsn_status = extc
|
|
810
811
|
rcpt.dsn_smtp_response = response.join(' ')
|
|
811
812
|
rcpt.dsn_remote_mta = mx.exchange
|
|
812
|
-
}
|
|
813
|
+
}
|
|
813
814
|
send_command('QUIT')
|
|
814
815
|
processing_mail = false
|
|
815
816
|
return self.bounce(reason, { mx })
|
|
@@ -870,7 +871,7 @@ class HMailItem extends events.EventEmitter {
|
|
|
870
871
|
case 'mail':
|
|
871
872
|
last_recip = recipients[recip_index]
|
|
872
873
|
recip_index++
|
|
873
|
-
send_command('RCPT', `TO:${last_recip.format(!smtp_properties.
|
|
874
|
+
send_command('RCPT', `TO:${last_recip.format(!smtp_properties.smtputf8)}`)
|
|
874
875
|
break
|
|
875
876
|
case 'rcpt':
|
|
876
877
|
if (last_recip && code.match(/^250/)) {
|
|
@@ -886,7 +887,7 @@ class HMailItem extends events.EventEmitter {
|
|
|
886
887
|
} else {
|
|
887
888
|
last_recip = recipients[recip_index]
|
|
888
889
|
recip_index++
|
|
889
|
-
send_command('RCPT', `TO:${last_recip.format(!smtp_properties.
|
|
890
|
+
send_command('RCPT', `TO:${last_recip.format(!smtp_properties.smtputf8)}`)
|
|
890
891
|
}
|
|
891
892
|
break
|
|
892
893
|
case 'data': {
|
|
@@ -1035,7 +1036,7 @@ class HMailItem extends events.EventEmitter {
|
|
|
1035
1036
|
msgid: `<${utils.uuid()}@${net_utils.get_primary_host_name()}>`,
|
|
1036
1037
|
}
|
|
1037
1038
|
|
|
1038
|
-
|
|
1039
|
+
for (let line of bounce_msg_) {
|
|
1039
1040
|
line = line.replace(/\{(\w+)\}/g, (i, word) => values[word] || '?')
|
|
1040
1041
|
|
|
1041
1042
|
if (bounce_headers_done == false && line == '') {
|
|
@@ -1045,7 +1046,7 @@ class HMailItem extends events.EventEmitter {
|
|
|
1045
1046
|
} else if (bounce_headers_done == true) {
|
|
1046
1047
|
bounce_body_lines.push(line)
|
|
1047
1048
|
}
|
|
1048
|
-
}
|
|
1049
|
+
}
|
|
1049
1050
|
|
|
1050
1051
|
const escaped_chars = {
|
|
1051
1052
|
'&': 'amp',
|
|
@@ -1058,7 +1059,7 @@ class HMailItem extends events.EventEmitter {
|
|
|
1058
1059
|
}
|
|
1059
1060
|
const escape_pattern = new RegExp(`[${Object.keys(escaped_chars).join('')}]`, 'g')
|
|
1060
1061
|
|
|
1061
|
-
|
|
1062
|
+
for (let line of bounce_msg_html_) {
|
|
1062
1063
|
line = line.replace(/\{(\w+)\}/g, (i, word) => {
|
|
1063
1064
|
if (word in values) {
|
|
1064
1065
|
return String(values[word]).replace(escape_pattern, (m) => `&${escaped_chars[m]};`)
|
|
@@ -1068,18 +1069,18 @@ class HMailItem extends events.EventEmitter {
|
|
|
1068
1069
|
})
|
|
1069
1070
|
|
|
1070
1071
|
bounce_html_lines.push(line)
|
|
1071
|
-
}
|
|
1072
|
+
}
|
|
1072
1073
|
|
|
1073
|
-
|
|
1074
|
+
for (const line of bounce_msg_image_) {
|
|
1074
1075
|
bounce_image_lines.push(line)
|
|
1075
|
-
}
|
|
1076
|
+
}
|
|
1076
1077
|
|
|
1077
1078
|
const boundary = `boundary_${utils.uuid()}`
|
|
1078
1079
|
const bounce_body = []
|
|
1079
1080
|
|
|
1080
|
-
|
|
1081
|
+
for (const line of bounce_header_lines) {
|
|
1081
1082
|
bounce_body.push(`${line}${CRLF}`)
|
|
1082
|
-
}
|
|
1083
|
+
}
|
|
1083
1084
|
bounce_body.push(
|
|
1084
1085
|
`Content-Type: multipart/report; report-type=delivery-status;${CRLF} boundary="${boundary}"${CRLF}`,
|
|
1085
1086
|
)
|
|
@@ -1107,18 +1108,18 @@ class HMailItem extends events.EventEmitter {
|
|
|
1107
1108
|
bounce_body.push(`--${boundary}${boundary_incr}${CRLF}`)
|
|
1108
1109
|
bounce_body.push(`Content-Type: text/plain; charset=us-ascii${CRLF}`)
|
|
1109
1110
|
bounce_body.push(CRLF)
|
|
1110
|
-
|
|
1111
|
+
for (const line of bounce_body_lines) {
|
|
1111
1112
|
bounce_body.push(`${line}${CRLF}`)
|
|
1112
|
-
}
|
|
1113
|
+
}
|
|
1113
1114
|
bounce_body.push(CRLF)
|
|
1114
1115
|
|
|
1115
1116
|
if (bounce_html_lines.length > 1) {
|
|
1116
1117
|
bounce_body.push(`--${boundary}${boundary_incr}${CRLF}`)
|
|
1117
1118
|
bounce_body.push(`Content-Type: text/html; charset=us-ascii${CRLF}`)
|
|
1118
1119
|
bounce_body.push(CRLF)
|
|
1119
|
-
|
|
1120
|
+
for (const line of bounce_html_lines) {
|
|
1120
1121
|
bounce_body.push(`${line}${CRLF}`)
|
|
1121
|
-
}
|
|
1122
|
+
}
|
|
1122
1123
|
bounce_body.push(CRLF)
|
|
1123
1124
|
bounce_body.push(`--${boundary}${boundary_incr}--${CRLF}`)
|
|
1124
1125
|
|
|
@@ -1127,9 +1128,9 @@ class HMailItem extends events.EventEmitter {
|
|
|
1127
1128
|
bounce_body.push(`--${boundary}${boundary_incr}${CRLF}`)
|
|
1128
1129
|
//bounce_body.push(`Content-Type: text/html; charset=us-ascii${CRLF}`);
|
|
1129
1130
|
//bounce_body.push(CRLF);
|
|
1130
|
-
|
|
1131
|
+
for (const line of bounce_image_lines) {
|
|
1131
1132
|
bounce_body.push(`${line}${CRLF}`)
|
|
1132
|
-
}
|
|
1133
|
+
}
|
|
1133
1134
|
bounce_body.push(CRLF)
|
|
1134
1135
|
bounce_body.push(`--${boundary}${boundary_incr}--${CRLF}`)
|
|
1135
1136
|
}
|
|
@@ -1145,7 +1146,7 @@ class HMailItem extends events.EventEmitter {
|
|
|
1145
1146
|
if (this.todo.queue_time) {
|
|
1146
1147
|
bounce_body.push(`Arrival-Date: ${utils.date_to_str(new Date(this.todo.queue_time))}${CRLF}`)
|
|
1147
1148
|
}
|
|
1148
|
-
this.todo.rcpt_to
|
|
1149
|
+
for (const rcpt_to of this.todo.rcpt_to) {
|
|
1149
1150
|
bounce_body.push(CRLF)
|
|
1150
1151
|
bounce_body.push(`Final-Recipient: rfc822;${rcpt_to.address()}${CRLF}`)
|
|
1151
1152
|
let dsn_action = null
|
|
@@ -1207,16 +1208,16 @@ class HMailItem extends events.EventEmitter {
|
|
|
1207
1208
|
if (diag_code != null) {
|
|
1208
1209
|
bounce_body.push(`Diagnostic-Code: ${diag_code}${CRLF}`)
|
|
1209
1210
|
}
|
|
1210
|
-
}
|
|
1211
|
+
}
|
|
1211
1212
|
bounce_body.push(CRLF)
|
|
1212
1213
|
|
|
1213
1214
|
bounce_body.push(`--${boundary}${CRLF}`)
|
|
1214
1215
|
bounce_body.push(`Content-Description: Undelivered Message Headers${CRLF}`)
|
|
1215
1216
|
bounce_body.push(`Content-Type: text/rfc822-headers${CRLF}`)
|
|
1216
1217
|
bounce_body.push(CRLF)
|
|
1217
|
-
header.header_list
|
|
1218
|
+
for (const line of header.header_list) {
|
|
1218
1219
|
bounce_body.push(line)
|
|
1219
|
-
}
|
|
1220
|
+
}
|
|
1220
1221
|
bounce_body.push(CRLF)
|
|
1221
1222
|
|
|
1222
1223
|
bounce_body.push(`--${boundary}--${CRLF}`)
|
|
@@ -1321,12 +1322,12 @@ class HMailItem extends events.EventEmitter {
|
|
|
1321
1322
|
}
|
|
1322
1323
|
|
|
1323
1324
|
convert_temp_failed_to_bounce(err, extra) {
|
|
1324
|
-
this.todo.rcpt_to
|
|
1325
|
+
for (const rcpt_to of this.todo.rcpt_to) {
|
|
1325
1326
|
rcpt_to.dsn_action = 'failed'
|
|
1326
1327
|
if (rcpt_to.dsn_status) {
|
|
1327
1328
|
rcpt_to.dsn_status = `${rcpt_to.dsn_status}`.replace(/^4/, '5')
|
|
1328
1329
|
}
|
|
1329
|
-
}
|
|
1330
|
+
}
|
|
1330
1331
|
return this.bounce(err, extra)
|
|
1331
1332
|
}
|
|
1332
1333
|
|
|
@@ -1445,9 +1446,9 @@ class HMailItem extends events.EventEmitter {
|
|
|
1445
1446
|
})
|
|
1446
1447
|
function err_handler(err, location) {
|
|
1447
1448
|
logger.error(this, `Error while splitting to new recipients (${location}): ${err}`)
|
|
1448
|
-
hmail.todo.rcpt_to
|
|
1449
|
+
for (const rcpt of hmail.todo.rcpt_to) {
|
|
1449
1450
|
hmail.extend_rcpt_with_dsn(rcpt, DSN.sys_unspecified(`Error splitting to new recipients: ${err}`))
|
|
1450
|
-
}
|
|
1451
|
+
}
|
|
1451
1452
|
hmail.bounce(`Error splitting to new recipients: ${err}`)
|
|
1452
1453
|
}
|
|
1453
1454
|
|
|
@@ -1485,9 +1486,9 @@ class HMailItem extends events.EventEmitter {
|
|
|
1485
1486
|
ws.on('error', (err) => {
|
|
1486
1487
|
logger.error(this, `Unable to write queue file (${fname}): ${err}`)
|
|
1487
1488
|
ws.destroy()
|
|
1488
|
-
hmail.todo.rcpt_to
|
|
1489
|
+
for (const rcpt of hmail.todo.rcpt_to) {
|
|
1489
1490
|
hmail.extend_rcpt_with_dsn(rcpt, DSN.sys_unspecified(`Error re-queueing some recipients: ${err}`))
|
|
1490
|
-
}
|
|
1491
|
+
}
|
|
1491
1492
|
hmail.bounce(`Error re-queueing some recipients: ${err}`)
|
|
1492
1493
|
})
|
|
1493
1494
|
|
package/outbound/index.js
CHANGED
|
@@ -200,16 +200,16 @@ function get_deliveries(transaction) {
|
|
|
200
200
|
|
|
201
201
|
// First get each domain
|
|
202
202
|
const recips = {}
|
|
203
|
-
transaction.rcpt_to
|
|
203
|
+
for (const rcpt of transaction.rcpt_to) {
|
|
204
204
|
const domain = rcpt.host
|
|
205
205
|
if (!recips[domain]) {
|
|
206
206
|
recips[domain] = []
|
|
207
207
|
}
|
|
208
208
|
recips[domain].push(rcpt)
|
|
209
|
-
}
|
|
210
|
-
Object.keys(recips)
|
|
209
|
+
}
|
|
210
|
+
for (const domain of Object.keys(recips)) {
|
|
211
211
|
deliveries.push({ domain, rcpts: recips[domain] })
|
|
212
|
-
}
|
|
212
|
+
}
|
|
213
213
|
return deliveries
|
|
214
214
|
}
|
|
215
215
|
|
|
@@ -248,6 +248,9 @@ exports.send_trans_email = function (transaction, next) {
|
|
|
248
248
|
|
|
249
249
|
let todo_index = 1
|
|
250
250
|
|
|
251
|
+
// See haraka/Haraka#3551
|
|
252
|
+
await new Promise((resolve) => setImmediate(resolve))
|
|
253
|
+
|
|
251
254
|
try {
|
|
252
255
|
for (const deliv of deliveries) {
|
|
253
256
|
const todo = new TODOItem(deliv.domain, deliv.rcpts, transaction)
|
package/outbound/tls.js
CHANGED
|
@@ -8,18 +8,6 @@ const hkredis = require('haraka-plugin-redis')
|
|
|
8
8
|
const logger = require('../logger')
|
|
9
9
|
const tls_socket = require('../tls_socket')
|
|
10
10
|
|
|
11
|
-
const inheritable_opts = [
|
|
12
|
-
'key',
|
|
13
|
-
'cert',
|
|
14
|
-
'ciphers',
|
|
15
|
-
'minVersion',
|
|
16
|
-
'dhparam',
|
|
17
|
-
'requestCert',
|
|
18
|
-
'honorCipherOrder',
|
|
19
|
-
'rejectUnauthorized',
|
|
20
|
-
'force_tls_hosts',
|
|
21
|
-
]
|
|
22
|
-
|
|
23
11
|
class OutboundTLS {
|
|
24
12
|
constructor() {
|
|
25
13
|
this.config = config
|
|
@@ -34,37 +22,8 @@ class OutboundTLS {
|
|
|
34
22
|
|
|
35
23
|
load_config() {
|
|
36
24
|
const tls_cfg = tls_socket.load_tls_ini({ role: 'client' })
|
|
37
|
-
|
|
38
|
-
cfg.redis = tls_cfg.redis //
|
|
39
|
-
|
|
40
|
-
for (const opt of inheritable_opts) {
|
|
41
|
-
if (cfg[opt] !== undefined) continue // option set in [outbound]
|
|
42
|
-
if (tls_cfg.main[opt] === undefined) continue // opt unset in tls.ini[main]
|
|
43
|
-
cfg[opt] = tls_cfg.main[opt] // use value from [main] section
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (cfg.key) {
|
|
47
|
-
if (Array.isArray(cfg.key)) {
|
|
48
|
-
cfg.key = cfg.key[0]
|
|
49
|
-
}
|
|
50
|
-
cfg.key = this.config.get(cfg.key, 'binary')
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (cfg.dhparam) {
|
|
54
|
-
cfg.dhparam = this.config.get(cfg.dhparam, 'binary')
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (cfg.cert) {
|
|
58
|
-
if (Array.isArray(cfg.cert)) {
|
|
59
|
-
cfg.cert = cfg.cert[0]
|
|
60
|
-
}
|
|
61
|
-
cfg.cert = this.config.get(cfg.cert, 'binary')
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (!cfg.no_tls_hosts) cfg.no_tls_hosts = []
|
|
65
|
-
if (!cfg.force_tls_hosts) cfg.force_tls_hosts = []
|
|
66
|
-
|
|
67
|
-
this.cfg = cfg
|
|
25
|
+
this.cfg = tls_socket.load_plugin_tls_options(tls_cfg.outbound || {})
|
|
26
|
+
this.cfg.redis = tls_cfg.redis // outbound-only: TLS NO-GO db (don't clone — has methods)
|
|
68
27
|
}
|
|
69
28
|
|
|
70
29
|
init(cb) {
|