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/connection.js
CHANGED
|
@@ -12,7 +12,7 @@ const constants = require('haraka-constants')
|
|
|
12
12
|
const net_utils = require('haraka-net-utils')
|
|
13
13
|
const Notes = require('haraka-notes')
|
|
14
14
|
const utils = require('haraka-utils')
|
|
15
|
-
const { Address } = require('address
|
|
15
|
+
const { Address } = require('./address')
|
|
16
16
|
const ResultStore = require('haraka-results')
|
|
17
17
|
|
|
18
18
|
// Haraka libs
|
|
@@ -329,7 +329,7 @@ class Connection {
|
|
|
329
329
|
} catch (err) {
|
|
330
330
|
if (err.stack) {
|
|
331
331
|
this.logerror(`${method} failed: ${err}`)
|
|
332
|
-
err.stack.split('\n')
|
|
332
|
+
for (const line of err.stack.split('\n')) this.logerror(line)
|
|
333
333
|
} else {
|
|
334
334
|
this.logerror(`${method} failed: ${err}`)
|
|
335
335
|
}
|
|
@@ -1039,7 +1039,7 @@ class Connection {
|
|
|
1039
1039
|
this.lognotice(dmsg, {
|
|
1040
1040
|
code: constants.translate(retval === constants.cont ? constants.ok : retval),
|
|
1041
1041
|
msg: msg || '',
|
|
1042
|
-
sender: this.transaction.mail_from.address
|
|
1042
|
+
sender: this.transaction.mail_from.address,
|
|
1043
1043
|
})
|
|
1044
1044
|
switch (retval) {
|
|
1045
1045
|
case constants.deny:
|
|
@@ -1089,7 +1089,7 @@ class Connection {
|
|
|
1089
1089
|
this.lognotice(dmsg, {
|
|
1090
1090
|
code: constants.translate(retval === constants.cont ? constants.ok : retval),
|
|
1091
1091
|
msg: msg || '',
|
|
1092
|
-
sender: this.transaction.mail_from.address
|
|
1092
|
+
sender: this.transaction.mail_from.address,
|
|
1093
1093
|
})
|
|
1094
1094
|
}
|
|
1095
1095
|
switch (retval) {
|
|
@@ -1234,6 +1234,13 @@ class Connection {
|
|
|
1234
1234
|
if (!host) {
|
|
1235
1235
|
return this.respond(501, 'HELO requires domain/address - see RFC-2821 4.1.1.1')
|
|
1236
1236
|
}
|
|
1237
|
+
// RFC 5321 §4.1.1.1: the domain/address-literal cannot contain
|
|
1238
|
+
// control characters. process_line() only strips the first \r?\n,
|
|
1239
|
+
// so a bare \r could otherwise survive into hello.host and the
|
|
1240
|
+
// generated Received: header / logs (header injection).
|
|
1241
|
+
if (/[\x00-\x1f\x7f]/.test(host)) {
|
|
1242
|
+
return this.respond(501, 'HELO syntax error - see RFC-2821 4.1.1.1')
|
|
1243
|
+
}
|
|
1237
1244
|
|
|
1238
1245
|
this.reset_transaction(() => {
|
|
1239
1246
|
this.set('hello', 'verb', 'HELO')
|
|
@@ -1248,6 +1255,10 @@ class Connection {
|
|
|
1248
1255
|
if (!host) {
|
|
1249
1256
|
return this.respond(501, 'EHLO requires domain/address - see RFC-2821 4.1.1.1')
|
|
1250
1257
|
}
|
|
1258
|
+
// RFC 5321 §4.1.1.1: reject control chars (see cmd_helo).
|
|
1259
|
+
if (/[\x00-\x1f\x7f]/.test(host)) {
|
|
1260
|
+
return this.respond(501, 'EHLO syntax error - see RFC-2821 4.1.1.1')
|
|
1261
|
+
}
|
|
1251
1262
|
|
|
1252
1263
|
this.reset_transaction(() => {
|
|
1253
1264
|
this.set('hello', 'verb', 'EHLO')
|
|
@@ -1320,10 +1331,10 @@ class Connection {
|
|
|
1320
1331
|
|
|
1321
1332
|
// Get rest of key=value pairs
|
|
1322
1333
|
const params = {}
|
|
1323
|
-
|
|
1334
|
+
for (const param of results) {
|
|
1324
1335
|
const kv = param.match(/^([^=]+)(?:=(.+))?$/)
|
|
1325
1336
|
if (kv) params[kv[1].toUpperCase()] = kv[2] || null
|
|
1326
|
-
}
|
|
1337
|
+
}
|
|
1327
1338
|
|
|
1328
1339
|
// Parameters are only valid if EHLO was sent
|
|
1329
1340
|
if (!this.esmtp && Object.keys(params).length > 0) {
|
|
@@ -1379,10 +1390,10 @@ class Connection {
|
|
|
1379
1390
|
|
|
1380
1391
|
// Get rest of key=value pairs
|
|
1381
1392
|
const params = {}
|
|
1382
|
-
|
|
1393
|
+
for (const param of results) {
|
|
1383
1394
|
const kv = param.match(/^([^=]+)(?:=(.+))?$/)
|
|
1384
1395
|
if (kv) params[kv[1].toUpperCase()] = kv[2] || null
|
|
1385
|
-
}
|
|
1396
|
+
}
|
|
1386
1397
|
|
|
1387
1398
|
// Parameters are only valid if EHLO was sent
|
|
1388
1399
|
if (!this.esmtp && Object.keys(params).length > 0) {
|
|
@@ -1433,17 +1444,23 @@ class Connection {
|
|
|
1433
1444
|
this.transaction.notes.authentication_results = []
|
|
1434
1445
|
}
|
|
1435
1446
|
|
|
1447
|
+
// Strip CR/LF and other control chars: an attacker-influenced
|
|
1448
|
+
// value (e.g. a failed AUTH username, see auth_base) must not be
|
|
1449
|
+
// able to inject extra header lines into Authentication-Results.
|
|
1450
|
+
// The legitimate folding (;\r\n\t) is added by the join below.
|
|
1451
|
+
const ar_clean = (s) => String(s).replace(/[\x00-\x1f\x7f]/g, '')
|
|
1452
|
+
|
|
1436
1453
|
// if message, store it in the appropriate note
|
|
1437
1454
|
if (message) {
|
|
1438
1455
|
if (has_tran === true) {
|
|
1439
|
-
this.transaction.notes.authentication_results.push(message)
|
|
1456
|
+
this.transaction.notes.authentication_results.push(ar_clean(message))
|
|
1440
1457
|
} else {
|
|
1441
|
-
this.notes.authentication_results.push(message)
|
|
1458
|
+
this.notes.authentication_results.push(ar_clean(message))
|
|
1442
1459
|
}
|
|
1443
1460
|
}
|
|
1444
1461
|
|
|
1445
1462
|
// assemble the new header
|
|
1446
|
-
let header = [this.local.host, ...this.notes.authentication_results]
|
|
1463
|
+
let header = [ar_clean(this.local.host), ...this.notes.authentication_results]
|
|
1447
1464
|
if (has_tran === true) {
|
|
1448
1465
|
header = [...header, ...this.transaction.notes.authentication_results]
|
|
1449
1466
|
}
|
package/docs/Outbound.md
CHANGED
|
@@ -203,7 +203,7 @@ Options accepted by `send_email(from, to, contents, next, options)`:
|
|
|
203
203
|
|
|
204
204
|
To send an already-built `Transaction` directly, use `outbound.send_trans_email(transaction, next)`. This is what `send_email()` calls internally and fires the `pre_send_trans_email` hook.
|
|
205
205
|
|
|
206
|
-
<a name="fn1">1</a>: `Address` objects are [address
|
|
206
|
+
<a name="fn1">1</a>: `Address` objects are [@haraka/email-address](https://github.com/haraka/email-address) objects.
|
|
207
207
|
|
|
208
208
|
[url-tls]: plugins/tls.md
|
|
209
209
|
[url-harakamx]: https://github.com/haraka/haraka-net-utils?tab=readme-ov-file#harakamx
|
package/docs/Transaction.md
CHANGED
|
@@ -132,4 +132,4 @@ Append a banner to the end of the message. If `html` is omitted, each newline in
|
|
|
132
132
|
|
|
133
133
|
Register a filter applied to body parts. `ct_match` is either a regex matched against the content-type line, or a string matched as a prefix (e.g. `/^text\/html/` or `'text/plain'`). `filter` receives `(content_type, encoding, buffer)` and must return a `Buffer` with the replacement body (in the same encoding).
|
|
134
134
|
|
|
135
|
-
[address]: https://github.com/haraka/
|
|
135
|
+
[address]: https://github.com/haraka/email-address
|
|
@@ -38,9 +38,7 @@ Configuration is stored in smtp_forward.ini in the following keys:
|
|
|
38
38
|
|
|
39
39
|
- enable_tls=[true]
|
|
40
40
|
|
|
41
|
-
Enable TLS with the forward host (if
|
|
42
|
-
|
|
43
|
-
This option controls the use of TLS via `STARTTLS`. This plugin does not work with SMTP over TLS.
|
|
41
|
+
Enable opportunistic TLS with the forward host via `STARTTLS` (if the host advertises it). This plugin does not work with implicit SMTP over TLS.
|
|
44
42
|
|
|
45
43
|
- auth_type=[plain\|login]
|
|
46
44
|
|
|
@@ -69,6 +67,24 @@ Configuration is stored in smtp_forward.ini in the following keys:
|
|
|
69
67
|
[example.com]
|
|
70
68
|
[example.net]
|
|
71
69
|
|
|
70
|
+
- [tls]
|
|
71
|
+
|
|
72
|
+
Client STARTTLS options are assembled by merging:
|
|
73
|
+
|
|
74
|
+
1. `tls.ini` `[main]` — the global Haraka TLS config
|
|
75
|
+
2. `smtp_forward.ini` `[tls]` — overrides. Anything set here wins.
|
|
76
|
+
|
|
77
|
+
Example `smtp_forward.ini` `[tls]` section:
|
|
78
|
+
|
|
79
|
+
[tls]
|
|
80
|
+
rejectUnauthorized=true
|
|
81
|
+
minVersion=TLSv1.2
|
|
82
|
+
no_tls_hosts[]=10.0.0.5
|
|
83
|
+
|
|
84
|
+
Per-domain `enable_tls=false` still disables STARTTLS for that backend. Per-domain TLS cipher/cert overrides are not currently supported.
|
|
85
|
+
|
|
86
|
+
Changes to `tls.ini` require a Haraka restart to apply to the forward path; changes to `smtp_forward.ini` are picked up by the existing reload hook.
|
|
87
|
+
|
|
72
88
|
# Per-Domain Configuration
|
|
73
89
|
|
|
74
90
|
More specific forward routes for domains can be defined. The domain is chosen based on the value of the `domain_selector` config variable.
|
|
@@ -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.
|
package/docs/plugins/status.md
CHANGED
|
@@ -11,15 +11,31 @@ This plugin allows to get internal status of queues and pools with SMTP commands
|
|
|
11
11
|
|
|
12
12
|
```
|
|
13
13
|
< 220 example.com ESMTP Haraka ready
|
|
14
|
-
> STATUS QUEUE
|
|
14
|
+
> STATUS QUEUE INSPECT
|
|
15
15
|
< 211 {"delivery_queue":[],"temp_fail_queue":[]}
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
## Available commands list
|
|
19
19
|
|
|
20
|
-
- `STATUS POOL LIST` -
|
|
21
|
-
- `STATUS QUEUE STATS` - queue statistics in format "<in_progress>/<delivery_queue length>/<temp_fail_queue length>"
|
|
22
|
-
- `STATUS QUEUE LIST` - list of
|
|
23
|
-
- `STATUS QUEUE INSPECT` - returns content of
|
|
20
|
+
- `STATUS POOL LIST` - map of active outbound connection pools, keyed by `host:port`
|
|
21
|
+
- `STATUS QUEUE STATS` - queue statistics in format `"<in_progress>/<delivery_queue length>/<temp_fail_queue length>"`
|
|
22
|
+
- `STATUS QUEUE LIST` - list of queue files on disk with _uuid, domain, mail_from, rcpt_to_ attributes
|
|
23
|
+
- `STATUS QUEUE INSPECT` - returns merged content of `outbound.delivery_queue` and `outbound.temp_fail_queue` across all workers
|
|
24
24
|
- `STATUS QUEUE DISCARD file` - stop delivering email file
|
|
25
25
|
- `STATUS QUEUE PUSH file` - try to re-deliver email immediately
|
|
26
|
+
|
|
27
|
+
## Notes
|
|
28
|
+
|
|
29
|
+
### Live data only
|
|
30
|
+
|
|
31
|
+
`POOL LIST`, `QUEUE STATS`, and `QUEUE INSPECT` reflect live in-memory state. They show only messages currently being processed or waiting in the retry queue. `QUEUE LIST` reads queue files from disk and may show messages that have already been delivered if they haven't been cleaned up yet.
|
|
32
|
+
|
|
33
|
+
### Cluster mode
|
|
34
|
+
|
|
35
|
+
In cluster mode, `POOL LIST`, `QUEUE STATS`, and `QUEUE INSPECT` aggregate results from all worker processes into a single response:
|
|
36
|
+
|
|
37
|
+
- `POOL LIST` — pool maps from all workers are merged into one object
|
|
38
|
+
- `QUEUE STATS` — counters from all workers are summed into a single `"N/N/N"` string
|
|
39
|
+
- `QUEUE INSPECT` — `delivery_queue` and `temp_fail_queue` arrays from all workers are concatenated
|
|
40
|
+
|
|
41
|
+
`QUEUE LIST` always runs on the master process since it reads shared queue files from disk.
|
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
|
@@ -7,7 +7,7 @@ const dns = require('node:dns')
|
|
|
7
7
|
const net = require('node:net')
|
|
8
8
|
const path = require('node:path')
|
|
9
9
|
|
|
10
|
-
const { Address } = require('address
|
|
10
|
+
const { Address } = require('../address')
|
|
11
11
|
const config = require('haraka-config')
|
|
12
12
|
const constants = require('haraka-constants')
|
|
13
13
|
const DSN = require('haraka-dsn')
|
|
@@ -266,12 +266,12 @@ class HMailItem extends events.EventEmitter {
|
|
|
266
266
|
}
|
|
267
267
|
|
|
268
268
|
async found_mx(mxs) {
|
|
269
|
-
// support
|
|
269
|
+
// support RFC 7505 null MX
|
|
270
270
|
if (mxs.length === 1 && mxs[0].priority === 0 && mxs[0].exchange === '') {
|
|
271
271
|
for (const rcpt of this.todo.rcpt_to) {
|
|
272
272
|
this.extend_rcpt_with_dsn(
|
|
273
273
|
rcpt,
|
|
274
|
-
DSN.
|
|
274
|
+
DSN.addr_null_mx(`Domain ${this.todo.domain} sends and receives no email (NULL MX)`),
|
|
275
275
|
)
|
|
276
276
|
}
|
|
277
277
|
return this.bounce(`Domain ${this.todo.domain} sends and receives no email (NULL MX)`)
|
|
@@ -459,7 +459,7 @@ class HMailItem extends events.EventEmitter {
|
|
|
459
459
|
} else if (r.toUpperCase() === 'ENHANCEDSTATUSCODES') {
|
|
460
460
|
smtp_properties.enh_status_codes = true
|
|
461
461
|
} else if (r.toUpperCase() === 'SMTPUTF8') {
|
|
462
|
-
smtp_properties.
|
|
462
|
+
smtp_properties.smtputf8 = true
|
|
463
463
|
} else {
|
|
464
464
|
// Check for SIZE parameter and limit
|
|
465
465
|
let matches = r.match(/^SIZE\s+(\d+)$/)
|
|
@@ -476,9 +476,9 @@ class HMailItem extends events.EventEmitter {
|
|
|
476
476
|
}
|
|
477
477
|
|
|
478
478
|
function get_reverse_path_with_params() {
|
|
479
|
-
const rp = self.todo.mail_from.format(!smtp_properties.
|
|
479
|
+
const rp = self.todo.mail_from.format(!smtp_properties.smtputf8)
|
|
480
480
|
let rp_params = ''
|
|
481
|
-
if (smtp_properties.
|
|
481
|
+
if (smtp_properties.smtputf8 && has_non_ascii(rp)) rp_params += ' SMTPUTF8'
|
|
482
482
|
return `FROM:${rp}${rp_params}`
|
|
483
483
|
}
|
|
484
484
|
|
|
@@ -678,12 +678,12 @@ class HMailItem extends events.EventEmitter {
|
|
|
678
678
|
processing_mail = false
|
|
679
679
|
// Release back to the pool and instruct it to terminate this connection
|
|
680
680
|
client_pool.release_client(socket, mx)
|
|
681
|
-
self.todo.rcpt_to
|
|
681
|
+
for (const rcpt of self.todo.rcpt_to) {
|
|
682
682
|
self.extend_rcpt_with_dsn(
|
|
683
683
|
rcpt,
|
|
684
684
|
DSN.proto_invalid_command(`Unrecognized response from upstream server: ${line}`),
|
|
685
685
|
)
|
|
686
|
-
}
|
|
686
|
+
}
|
|
687
687
|
self.bounce(`Unrecognized response from upstream server: ${line}`, { mx })
|
|
688
688
|
return
|
|
689
689
|
}
|
|
@@ -720,14 +720,14 @@ class HMailItem extends events.EventEmitter {
|
|
|
720
720
|
}
|
|
721
721
|
// Error
|
|
722
722
|
reason = response.join(' ')
|
|
723
|
-
|
|
723
|
+
for (const rcpt of recipients) {
|
|
724
724
|
rcpt.dsn_action = 'delayed'
|
|
725
725
|
rcpt.dsn_smtp_code = code
|
|
726
726
|
rcpt.dsn_smtp_extc = extc
|
|
727
727
|
rcpt.dsn_status = extc
|
|
728
728
|
rcpt.dsn_smtp_response = response.join(' ')
|
|
729
729
|
rcpt.dsn_remote_mta = mx.exchange
|
|
730
|
-
}
|
|
730
|
+
}
|
|
731
731
|
send_command('QUIT')
|
|
732
732
|
processing_mail = false
|
|
733
733
|
return self.temp_fail(`Upstream error: ${code} ${extc ? `${extc} ` : ''}${reason}`)
|
|
@@ -756,14 +756,14 @@ class HMailItem extends events.EventEmitter {
|
|
|
756
756
|
}
|
|
757
757
|
} else if (processing_mail) {
|
|
758
758
|
reason = response.join(' ')
|
|
759
|
-
|
|
759
|
+
for (const rcpt of recipients) {
|
|
760
760
|
rcpt.dsn_action = 'delayed'
|
|
761
761
|
rcpt.dsn_smtp_code = code
|
|
762
762
|
rcpt.dsn_smtp_extc = extc
|
|
763
763
|
rcpt.dsn_status = extc
|
|
764
764
|
rcpt.dsn_smtp_response = response.join(' ')
|
|
765
765
|
rcpt.dsn_remote_mta = mx.exchange
|
|
766
|
-
}
|
|
766
|
+
}
|
|
767
767
|
send_command('QUIT')
|
|
768
768
|
processing_mail = false
|
|
769
769
|
return self.temp_fail(`Upstream error: ${code} ${extc ? `${extc} ` : ''}${reason}`)
|
|
@@ -803,14 +803,14 @@ class HMailItem extends events.EventEmitter {
|
|
|
803
803
|
}
|
|
804
804
|
}
|
|
805
805
|
} else {
|
|
806
|
-
|
|
806
|
+
for (const rcpt of recipients) {
|
|
807
807
|
rcpt.dsn_action = 'failed'
|
|
808
808
|
rcpt.dsn_smtp_code = code
|
|
809
809
|
rcpt.dsn_smtp_extc = extc
|
|
810
810
|
rcpt.dsn_status = extc
|
|
811
811
|
rcpt.dsn_smtp_response = response.join(' ')
|
|
812
812
|
rcpt.dsn_remote_mta = mx.exchange
|
|
813
|
-
}
|
|
813
|
+
}
|
|
814
814
|
send_command('QUIT')
|
|
815
815
|
processing_mail = false
|
|
816
816
|
return self.bounce(reason, { mx })
|
|
@@ -871,7 +871,7 @@ class HMailItem extends events.EventEmitter {
|
|
|
871
871
|
case 'mail':
|
|
872
872
|
last_recip = recipients[recip_index]
|
|
873
873
|
recip_index++
|
|
874
|
-
send_command('RCPT', `TO:${last_recip.format(!smtp_properties.
|
|
874
|
+
send_command('RCPT', `TO:${last_recip.format(!smtp_properties.smtputf8)}`)
|
|
875
875
|
break
|
|
876
876
|
case 'rcpt':
|
|
877
877
|
if (last_recip && code.match(/^250/)) {
|
|
@@ -887,7 +887,7 @@ class HMailItem extends events.EventEmitter {
|
|
|
887
887
|
} else {
|
|
888
888
|
last_recip = recipients[recip_index]
|
|
889
889
|
recip_index++
|
|
890
|
-
send_command('RCPT', `TO:${last_recip.format(!smtp_properties.
|
|
890
|
+
send_command('RCPT', `TO:${last_recip.format(!smtp_properties.smtputf8)}`)
|
|
891
891
|
}
|
|
892
892
|
break
|
|
893
893
|
case 'data': {
|
|
@@ -1036,7 +1036,7 @@ class HMailItem extends events.EventEmitter {
|
|
|
1036
1036
|
msgid: `<${utils.uuid()}@${net_utils.get_primary_host_name()}>`,
|
|
1037
1037
|
}
|
|
1038
1038
|
|
|
1039
|
-
|
|
1039
|
+
for (let line of bounce_msg_) {
|
|
1040
1040
|
line = line.replace(/\{(\w+)\}/g, (i, word) => values[word] || '?')
|
|
1041
1041
|
|
|
1042
1042
|
if (bounce_headers_done == false && line == '') {
|
|
@@ -1046,7 +1046,7 @@ class HMailItem extends events.EventEmitter {
|
|
|
1046
1046
|
} else if (bounce_headers_done == true) {
|
|
1047
1047
|
bounce_body_lines.push(line)
|
|
1048
1048
|
}
|
|
1049
|
-
}
|
|
1049
|
+
}
|
|
1050
1050
|
|
|
1051
1051
|
const escaped_chars = {
|
|
1052
1052
|
'&': 'amp',
|
|
@@ -1059,7 +1059,7 @@ class HMailItem extends events.EventEmitter {
|
|
|
1059
1059
|
}
|
|
1060
1060
|
const escape_pattern = new RegExp(`[${Object.keys(escaped_chars).join('')}]`, 'g')
|
|
1061
1061
|
|
|
1062
|
-
|
|
1062
|
+
for (let line of bounce_msg_html_) {
|
|
1063
1063
|
line = line.replace(/\{(\w+)\}/g, (i, word) => {
|
|
1064
1064
|
if (word in values) {
|
|
1065
1065
|
return String(values[word]).replace(escape_pattern, (m) => `&${escaped_chars[m]};`)
|
|
@@ -1069,18 +1069,18 @@ class HMailItem extends events.EventEmitter {
|
|
|
1069
1069
|
})
|
|
1070
1070
|
|
|
1071
1071
|
bounce_html_lines.push(line)
|
|
1072
|
-
}
|
|
1072
|
+
}
|
|
1073
1073
|
|
|
1074
|
-
|
|
1074
|
+
for (const line of bounce_msg_image_) {
|
|
1075
1075
|
bounce_image_lines.push(line)
|
|
1076
|
-
}
|
|
1076
|
+
}
|
|
1077
1077
|
|
|
1078
1078
|
const boundary = `boundary_${utils.uuid()}`
|
|
1079
1079
|
const bounce_body = []
|
|
1080
1080
|
|
|
1081
|
-
|
|
1081
|
+
for (const line of bounce_header_lines) {
|
|
1082
1082
|
bounce_body.push(`${line}${CRLF}`)
|
|
1083
|
-
}
|
|
1083
|
+
}
|
|
1084
1084
|
bounce_body.push(
|
|
1085
1085
|
`Content-Type: multipart/report; report-type=delivery-status;${CRLF} boundary="${boundary}"${CRLF}`,
|
|
1086
1086
|
)
|
|
@@ -1108,18 +1108,18 @@ class HMailItem extends events.EventEmitter {
|
|
|
1108
1108
|
bounce_body.push(`--${boundary}${boundary_incr}${CRLF}`)
|
|
1109
1109
|
bounce_body.push(`Content-Type: text/plain; charset=us-ascii${CRLF}`)
|
|
1110
1110
|
bounce_body.push(CRLF)
|
|
1111
|
-
|
|
1111
|
+
for (const line of bounce_body_lines) {
|
|
1112
1112
|
bounce_body.push(`${line}${CRLF}`)
|
|
1113
|
-
}
|
|
1113
|
+
}
|
|
1114
1114
|
bounce_body.push(CRLF)
|
|
1115
1115
|
|
|
1116
1116
|
if (bounce_html_lines.length > 1) {
|
|
1117
1117
|
bounce_body.push(`--${boundary}${boundary_incr}${CRLF}`)
|
|
1118
1118
|
bounce_body.push(`Content-Type: text/html; charset=us-ascii${CRLF}`)
|
|
1119
1119
|
bounce_body.push(CRLF)
|
|
1120
|
-
|
|
1120
|
+
for (const line of bounce_html_lines) {
|
|
1121
1121
|
bounce_body.push(`${line}${CRLF}`)
|
|
1122
|
-
}
|
|
1122
|
+
}
|
|
1123
1123
|
bounce_body.push(CRLF)
|
|
1124
1124
|
bounce_body.push(`--${boundary}${boundary_incr}--${CRLF}`)
|
|
1125
1125
|
|
|
@@ -1128,9 +1128,9 @@ class HMailItem extends events.EventEmitter {
|
|
|
1128
1128
|
bounce_body.push(`--${boundary}${boundary_incr}${CRLF}`)
|
|
1129
1129
|
//bounce_body.push(`Content-Type: text/html; charset=us-ascii${CRLF}`);
|
|
1130
1130
|
//bounce_body.push(CRLF);
|
|
1131
|
-
|
|
1131
|
+
for (const line of bounce_image_lines) {
|
|
1132
1132
|
bounce_body.push(`${line}${CRLF}`)
|
|
1133
|
-
}
|
|
1133
|
+
}
|
|
1134
1134
|
bounce_body.push(CRLF)
|
|
1135
1135
|
bounce_body.push(`--${boundary}${boundary_incr}--${CRLF}`)
|
|
1136
1136
|
}
|
|
@@ -1146,9 +1146,9 @@ class HMailItem extends events.EventEmitter {
|
|
|
1146
1146
|
if (this.todo.queue_time) {
|
|
1147
1147
|
bounce_body.push(`Arrival-Date: ${utils.date_to_str(new Date(this.todo.queue_time))}${CRLF}`)
|
|
1148
1148
|
}
|
|
1149
|
-
this.todo.rcpt_to
|
|
1149
|
+
for (const rcpt_to of this.todo.rcpt_to) {
|
|
1150
1150
|
bounce_body.push(CRLF)
|
|
1151
|
-
bounce_body.push(`Final-Recipient: rfc822;${rcpt_to.address
|
|
1151
|
+
bounce_body.push(`Final-Recipient: rfc822;${rcpt_to.address}${CRLF}`)
|
|
1152
1152
|
let dsn_action = null
|
|
1153
1153
|
if (rcpt_to.dsn_action) {
|
|
1154
1154
|
dsn_action = rcpt_to.dsn_action
|
|
@@ -1208,16 +1208,16 @@ class HMailItem extends events.EventEmitter {
|
|
|
1208
1208
|
if (diag_code != null) {
|
|
1209
1209
|
bounce_body.push(`Diagnostic-Code: ${diag_code}${CRLF}`)
|
|
1210
1210
|
}
|
|
1211
|
-
}
|
|
1211
|
+
}
|
|
1212
1212
|
bounce_body.push(CRLF)
|
|
1213
1213
|
|
|
1214
1214
|
bounce_body.push(`--${boundary}${CRLF}`)
|
|
1215
1215
|
bounce_body.push(`Content-Description: Undelivered Message Headers${CRLF}`)
|
|
1216
1216
|
bounce_body.push(`Content-Type: text/rfc822-headers${CRLF}`)
|
|
1217
1217
|
bounce_body.push(CRLF)
|
|
1218
|
-
header.header_list
|
|
1218
|
+
for (const line of header.header_list) {
|
|
1219
1219
|
bounce_body.push(line)
|
|
1220
|
-
}
|
|
1220
|
+
}
|
|
1221
1221
|
bounce_body.push(CRLF)
|
|
1222
1222
|
|
|
1223
1223
|
bounce_body.push(`--${boundary}--${CRLF}`)
|
|
@@ -1322,12 +1322,12 @@ class HMailItem extends events.EventEmitter {
|
|
|
1322
1322
|
}
|
|
1323
1323
|
|
|
1324
1324
|
convert_temp_failed_to_bounce(err, extra) {
|
|
1325
|
-
this.todo.rcpt_to
|
|
1325
|
+
for (const rcpt_to of this.todo.rcpt_to) {
|
|
1326
1326
|
rcpt_to.dsn_action = 'failed'
|
|
1327
1327
|
if (rcpt_to.dsn_status) {
|
|
1328
1328
|
rcpt_to.dsn_status = `${rcpt_to.dsn_status}`.replace(/^4/, '5')
|
|
1329
1329
|
}
|
|
1330
|
-
}
|
|
1330
|
+
}
|
|
1331
1331
|
return this.bounce(err, extra)
|
|
1332
1332
|
}
|
|
1333
1333
|
|
|
@@ -1446,9 +1446,9 @@ class HMailItem extends events.EventEmitter {
|
|
|
1446
1446
|
})
|
|
1447
1447
|
function err_handler(err, location) {
|
|
1448
1448
|
logger.error(this, `Error while splitting to new recipients (${location}): ${err}`)
|
|
1449
|
-
hmail.todo.rcpt_to
|
|
1449
|
+
for (const rcpt of hmail.todo.rcpt_to) {
|
|
1450
1450
|
hmail.extend_rcpt_with_dsn(rcpt, DSN.sys_unspecified(`Error splitting to new recipients: ${err}`))
|
|
1451
|
-
}
|
|
1451
|
+
}
|
|
1452
1452
|
hmail.bounce(`Error splitting to new recipients: ${err}`)
|
|
1453
1453
|
}
|
|
1454
1454
|
|
|
@@ -1486,9 +1486,9 @@ class HMailItem extends events.EventEmitter {
|
|
|
1486
1486
|
ws.on('error', (err) => {
|
|
1487
1487
|
logger.error(this, `Unable to write queue file (${fname}): ${err}`)
|
|
1488
1488
|
ws.destroy()
|
|
1489
|
-
hmail.todo.rcpt_to
|
|
1489
|
+
for (const rcpt of hmail.todo.rcpt_to) {
|
|
1490
1490
|
hmail.extend_rcpt_with_dsn(rcpt, DSN.sys_unspecified(`Error re-queueing some recipients: ${err}`))
|
|
1491
|
-
}
|
|
1491
|
+
}
|
|
1492
1492
|
hmail.bounce(`Error re-queueing some recipients: ${err}`)
|
|
1493
1493
|
})
|
|
1494
1494
|
|
package/outbound/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const fs = require('node:fs/promises')
|
|
4
4
|
const path = require('node:path')
|
|
5
5
|
|
|
6
|
-
const { Address } = require('address
|
|
6
|
+
const { Address } = require('../address')
|
|
7
7
|
const config = require('haraka-config')
|
|
8
8
|
const constants = require('haraka-constants')
|
|
9
9
|
const net_utils = require('haraka-net-utils')
|
|
@@ -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
|
|
package/outbound/queue.js
CHANGED
|
@@ -4,7 +4,7 @@ const child_process = require('node:child_process')
|
|
|
4
4
|
const fs = require('node:fs/promises')
|
|
5
5
|
const path = require('node:path')
|
|
6
6
|
|
|
7
|
-
const { Address } = require('address
|
|
7
|
+
const { Address } = require('../address')
|
|
8
8
|
const config = require('haraka-config')
|
|
9
9
|
|
|
10
10
|
const logger = require('../logger')
|
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) {
|