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.
Files changed (79) hide show
  1. package/.prettierignore +1 -1
  2. package/{Changes.md → CHANGELOG.md} +54 -3
  3. package/CONTRIBUTORS.md +26 -26
  4. package/Plugins.md +99 -99
  5. package/README.md +68 -93
  6. package/SECURITY.md +178 -0
  7. package/bin/haraka +7 -14
  8. package/config/plugins +0 -3
  9. package/config/smtp_forward.ini +10 -0
  10. package/config/smtp_proxy.ini +10 -0
  11. package/connection.js +25 -8
  12. package/docs/Connection.md +126 -39
  13. package/docs/CoreConfig.md +92 -74
  14. package/docs/HAProxy.md +41 -25
  15. package/docs/Logging.md +68 -38
  16. package/docs/Outbound.md +124 -179
  17. package/docs/Plugins.md +38 -59
  18. package/docs/Transaction.md +78 -83
  19. package/docs/Tutorial.md +122 -209
  20. package/docs/plugins/aliases.md +1 -141
  21. package/docs/plugins/auth/auth_ldap.md +2 -39
  22. package/docs/plugins/max_unrecognized_commands.md +4 -18
  23. package/docs/plugins/process_title.md +3 -3
  24. package/docs/plugins/queue/smtp_forward.md +19 -3
  25. package/docs/plugins/queue/smtp_proxy.md +10 -2
  26. package/docs/plugins/reseed_rng.md +11 -13
  27. package/docs/plugins/tls.md +7 -7
  28. package/docs/plugins/toobusy.md +10 -4
  29. package/docs/tutorials/SettingUpOutbound.md +40 -48
  30. package/endpoint.js +32 -2
  31. package/haraka.js +1 -1
  32. package/outbound/hmail.js +42 -41
  33. package/outbound/index.js +7 -4
  34. package/outbound/tls.js +2 -43
  35. package/package.json +51 -61
  36. package/plugins/auth/auth_base.js +9 -3
  37. package/plugins/auth/auth_proxy.js +14 -11
  38. package/plugins/block_me.js +4 -2
  39. package/plugins/prevent_credential_leaks.js +3 -1
  40. package/plugins/process_title.js +6 -6
  41. package/plugins/queue/qmail-queue.js +15 -19
  42. package/plugins/queue/smtp_forward.js +12 -4
  43. package/plugins/queue/smtp_proxy.js +14 -3
  44. package/plugins/tls.js +13 -5
  45. package/plugins/xclient.js +3 -1
  46. package/server.js +22 -10
  47. package/smtp_client.js +20 -11
  48. package/test/config/block_me.recipient +1 -0
  49. package/test/config/block_me.senders +1 -0
  50. package/test/connection.js +258 -0
  51. package/test/endpoint.js +27 -0
  52. package/test/outbound/bounce_net_errors.js +3 -2
  53. package/test/outbound/hmail.js +19 -0
  54. package/test/outbound/index.js +189 -0
  55. package/test/outbound/queue.js +92 -0
  56. package/test/plugins/auth/auth_bridge.js +80 -0
  57. package/test/plugins/auth/flat_file.js +128 -0
  58. package/test/plugins/block_me.js +157 -0
  59. package/test/plugins/data.signatures.js +114 -0
  60. package/test/plugins/delay_deny.js +263 -0
  61. package/test/plugins/prevent_credential_leaks.js +178 -0
  62. package/test/plugins/process_title.js +135 -0
  63. package/test/plugins/queue/deliver.js +99 -0
  64. package/test/plugins/queue/discard.js +79 -0
  65. package/test/plugins/queue/lmtp.js +138 -0
  66. package/test/plugins/queue/qmail-queue.js +99 -0
  67. package/test/plugins/queue/quarantine.js +81 -0
  68. package/test/plugins/queue/smtp_bridge.js +154 -0
  69. package/test/plugins/queue/smtp_forward.js +42 -6
  70. package/test/plugins/queue/smtp_proxy.js +139 -0
  71. package/test/plugins/reseed_rng.js +34 -0
  72. package/test/plugins/tarpit.js +91 -0
  73. package/test/plugins/tls.js +25 -0
  74. package/test/plugins/toobusy.js +21 -0
  75. package/test/plugins/xclient.js +14 -0
  76. package/test/server.js +231 -0
  77. package/test/smtp_client.js +45 -12
  78. package/test/tls_socket.js +220 -0
  79. 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 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.
@@ -1,18 +1,16 @@
1
1
  # reseed_rng
2
2
 
3
- The V8 that ships with node 0.4.x uses an unsophisticated method of
4
- seeding its random number generator- it simply uses the current time
5
- in ms. Worse, that version of V8 (at least) doesn't provide a way
6
- to explicitly reseed the RNG.
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
- In situations where multiple processes can spawn in the same
9
- ms, processes can be seeded with the same value, leading to bad
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
- This plugin uses David Bao's reseed.js (see http://davidbau.com/archives/2010/01/30/random_seeds_coded_hints_and_quintillions.html)
14
- to provide a reseedable Math.random(), and hooks the init_child event
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
- All users of the 'cluster' module should consider using this plugin.
14
+ ## Configuration
15
+
16
+ No configuration.
@@ -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 node's tls.DEFAULT_MIN_VERSION constant.
159
-
160
- (**Node.js 11.4+ required**, for older instances you can use _secureProtocol_ settings)
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 for handling the TLS session. Choose
199
- one of the methods described at the
200
- [OpenSSL API page](https://www.openssl.org/docs/manmaster/ssl/ssl.html).
201
- The default is `SSLv23_method`.
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
 
@@ -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 have to install the 'toobusy-js' module by running
9
- 'npm install toobusy-js' in your Haraka configuration directory.
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
- This plugin should be listed at the top of your config/plugins file so that
12
- it runs before any other plugin that hooks lookup_rdns.
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 trivially easy to configure Haraka as an outbound email server. But
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
- - Get your DNS PTR record working - make sure it matches the A record of the
7
- host you are sending from.
8
- - Consider implementing an SPF record. I don't personally do this, but some
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
- There's lots of information elsewhere on the internet about getting these
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
- ## First Some Background
11
+ ## Background
15
12
 
16
- Sending outbound mail through Haraka is called "relaying", and that is the
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
- ## The Setup
15
+ ## Setup
27
16
 
28
- Outbound mail servers should run on port 587 and enforce authentication. This
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
- Let's create a new Haraka instance:
19
+ Create a new Haraka instance:
36
20
 
37
- haraka -i haraka-outbound
38
- cd haraka-outbound
21
+ ```sh
22
+ haraka -i haraka-outbound
23
+ cd haraka-outbound
24
+ ```
39
25
 
40
- Now edit config/smtp.ini - change the port to 587.
26
+ In `config/smtp.ini`, set the listener:
41
27
 
42
- Next we setup our plugins - all we need is the tls and auth plugin. AUTH capability is only advertised after TLS/SSL negotiation (except for connections from the local host):
28
+ ```ini
29
+ listen=[::0]:465,[::0]:587
30
+ smtps_port=465
31
+ ```
43
32
 
44
- echo "tls
45
- auth/flat_file" > config/plugins
33
+ Anything in `smtps_port` runs implicit TLS; the other ports advertise `STARTTLS`.
46
34
 
47
- Now edit the flat file password file, and put in an appropriate username
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
- vi config/auth_flat_file.ini
37
+ ```sh
38
+ cat > config/plugins <<'EOF'
39
+ tls
40
+ auth/flat_file
41
+ EOF
42
+ ```
51
43
 
52
- See the documentation in docs/plugins/auth/flat_file.md for information about
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
- Now you can start Haraka. That's all the configuration you need.
46
+ Start Haraka:
56
47
 
57
- haraka -c .
48
+ ```sh
49
+ haraka -c .
50
+ ```
58
51
 
59
- Now in another window you can run swaks to test this - be sure to substitute
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
- swaks --to youremail@yourdomain.com --from test@example.com --server localhost \
64
- --port 587 --auth-user testuser --auth-password testpassword
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
- Watch the output of swaks and ensure no errors have occurred. Then watch
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
- You are done!
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 sockaddr = require('sockaddr')
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 = sockaddr(addr, { defaultPort })
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').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
@@ -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
- // we are fucked... guess I need somewhere for this to go
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.temp_fail('Error obtaining file size')
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 draft-delany-nullmx-02
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.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)`),
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.smtp_utf8 = true
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.smtp_utf8)
479
+ const rp = self.todo.mail_from.format(!smtp_properties.smtputf8)
479
480
  let rp_params = ''
480
- if (smtp_properties.smtp_utf8 && has_non_ascii(rp)) rp_params += ' SMTPUTF8'
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.forEach((rcpt) => {
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
- recipients.forEach((rcpt) => {
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
- recipients.forEach((rcpt) => {
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
- recipients.forEach((rcpt) => {
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.smtp_utf8)}`)
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.smtp_utf8)}`)
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
- bounce_msg_.forEach((line) => {
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
- bounce_msg_html_.forEach((line) => {
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
- bounce_msg_image_.forEach((line) => {
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
- bounce_header_lines.forEach((line) => {
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
- bounce_body_lines.forEach((line) => {
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
- bounce_html_lines.forEach((line) => {
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
- bounce_image_lines.forEach((line) => {
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.forEach((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.forEach((line) => {
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.forEach((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.forEach((rcpt) => {
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.forEach((rcpt) => {
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.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
 
@@ -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
- 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) {