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.
Files changed (69) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/CONTRIBUTORS.md +8 -8
  3. package/Plugins.md +99 -99
  4. package/address.js +53 -0
  5. package/bin/haraka +1 -1
  6. package/config/smtp_forward.ini +10 -0
  7. package/config/smtp_proxy.ini +10 -0
  8. package/connection.js +28 -11
  9. package/docs/Outbound.md +1 -1
  10. package/docs/Transaction.md +1 -1
  11. package/docs/plugins/queue/smtp_forward.md +19 -3
  12. package/docs/plugins/queue/smtp_proxy.md +10 -2
  13. package/docs/plugins/status.md +21 -5
  14. package/haraka.js +1 -1
  15. package/outbound/hmail.js +41 -41
  16. package/outbound/index.js +5 -5
  17. package/outbound/queue.js +1 -1
  18. package/outbound/tls.js +2 -43
  19. package/package.json +48 -48
  20. package/plugins/auth/auth_base.js +9 -3
  21. package/plugins/auth/auth_proxy.js +14 -11
  22. package/plugins/block_me.js +6 -4
  23. package/plugins/prevent_credential_leaks.js +3 -1
  24. package/plugins/process_title.js +6 -6
  25. package/plugins/queue/qmail-queue.js +15 -19
  26. package/plugins/queue/smtp_forward.js +14 -6
  27. package/plugins/queue/smtp_proxy.js +14 -3
  28. package/plugins/rcpt_to.host_list_base.js +1 -1
  29. package/plugins/record_envelope_addresses.js +2 -2
  30. package/plugins/status.js +34 -5
  31. package/plugins/tls.js +13 -5
  32. package/plugins/xclient.js +3 -1
  33. package/server.js +5 -3
  34. package/smtp_client.js +20 -11
  35. package/test/config/block_me.recipient +1 -0
  36. package/test/config/block_me.senders +1 -0
  37. package/test/connection.js +25 -1
  38. package/test/fixtures/util_hmailitem.js +1 -1
  39. package/test/outbound/bounce_net_errors.js +3 -2
  40. package/test/outbound/index.js +2 -2
  41. package/test/plugins/auth/auth_base.js +1 -1
  42. package/test/plugins/auth/auth_bridge.js +80 -0
  43. package/test/plugins/auth/flat_file.js +128 -0
  44. package/test/plugins/block_me.js +157 -0
  45. package/test/plugins/data.signatures.js +114 -0
  46. package/test/plugins/delay_deny.js +263 -0
  47. package/test/plugins/prevent_credential_leaks.js +178 -0
  48. package/test/plugins/process_title.js +135 -0
  49. package/test/plugins/queue/deliver.js +99 -0
  50. package/test/plugins/queue/discard.js +79 -0
  51. package/test/plugins/queue/lmtp.js +138 -0
  52. package/test/plugins/queue/qmail-queue.js +99 -0
  53. package/test/plugins/queue/quarantine.js +81 -0
  54. package/test/plugins/queue/smtp_bridge.js +154 -0
  55. package/test/plugins/queue/smtp_forward.js +43 -7
  56. package/test/plugins/queue/smtp_proxy.js +139 -0
  57. package/test/plugins/rcpt_to.host_list_base.js +1 -1
  58. package/test/plugins/rcpt_to.in_host_list.js +1 -1
  59. package/test/plugins/record_envelope_addresses.js +2 -2
  60. package/test/plugins/reseed_rng.js +34 -0
  61. package/test/plugins/status.js +71 -0
  62. package/test/plugins/tarpit.js +91 -0
  63. package/test/plugins/tls.js +25 -0
  64. package/test/plugins/toobusy.js +21 -0
  65. package/test/plugins/xclient.js +14 -0
  66. package/test/server.js +59 -0
  67. package/test/smtp_client.js +46 -13
  68. package/test/tls_socket.js +82 -0
  69. 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-rfc2821')
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').forEach(this.logerror)
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
- results.forEach((param) => {
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
- results.forEach((param) => {
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-rfc2821](https://github.com/haraka/node-address-rfc2821) objects.
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
@@ -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/node-address-rfc2821
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 supported). TLS uses options from the tls plugin. If key and cert are provided in the the outbound section of the tls plugin, that certificate will be used as a TLS Client Certificate.
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 supported). TLS uses options from
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.
@@ -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 LIST
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` - list of active pools
21
- - `STATUS QUEUE STATS` - queue statistics in format "<in_progress>/<delivery_queue length>/<temp_fail_queue length>"
22
- - `STATUS QUEUE LIST` - list of parsed queue files with _uuid, domain, mail_from, rcpt_to_ attributes
23
- - `STATUS QUEUE INSPECT` - returns content of _outbound.delivery_queue_ and _outbound.temp_fail_queue_
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').forEach((line) => logger.crit(line))
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-rfc2821')
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 draft-delany-nullmx-02
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.addr_bad_dest_system(`Domain ${this.todo.domain} sends and receives no email (NULL MX)`),
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.smtp_utf8 = true
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.smtp_utf8)
479
+ const rp = self.todo.mail_from.format(!smtp_properties.smtputf8)
480
480
  let rp_params = ''
481
- if (smtp_properties.smtp_utf8 && has_non_ascii(rp)) rp_params += ' SMTPUTF8'
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.forEach((rcpt) => {
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
- recipients.forEach((rcpt) => {
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
- recipients.forEach((rcpt) => {
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
- recipients.forEach((rcpt) => {
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.smtp_utf8)}`)
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.smtp_utf8)}`)
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
- bounce_msg_.forEach((line) => {
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
- bounce_msg_html_.forEach((line) => {
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
- bounce_msg_image_.forEach((line) => {
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
- bounce_header_lines.forEach((line) => {
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
- bounce_body_lines.forEach((line) => {
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
- bounce_html_lines.forEach((line) => {
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
- bounce_image_lines.forEach((line) => {
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.forEach((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()}${CRLF}`)
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.forEach((line) => {
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.forEach((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.forEach((rcpt) => {
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.forEach((rcpt) => {
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-rfc2821')
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.forEach((rcpt) => {
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).forEach((domain) => {
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-rfc2821')
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
- const cfg = JSON.parse(JSON.stringify(tls_cfg.outbound || {}))
38
- cfg.redis = tls_cfg.redis // Don't clone - contains methods
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) {