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
package/bin/haraka CHANGED
@@ -11,7 +11,6 @@ const os = require('node:os')
11
11
 
12
12
  const nopt = require('nopt')
13
13
  const utils = require('haraka-utils')
14
- const sprintf = require('sprintf-js').sprintf
15
14
  const base = path.join(__dirname, '..')
16
15
  const ver = utils.getVersion(base)
17
16
  const knownOpts = {
@@ -313,15 +312,7 @@ if (parsed.version) {
313
312
  ;(async () => {
314
313
  const qlist = await outbound.list_queue()
315
314
  for (const todo of qlist) {
316
- console.log(
317
- sprintf(
318
- 'Q: %s rcpt:%d from:%s domain:%s',
319
- todo.file,
320
- todo.rcpt_to.length,
321
- todo.mail_from.toString(),
322
- todo.domain,
323
- ),
324
- )
315
+ console.log(`Q: ${todo.file} rcpt:${todo.rcpt_to.length} from:${todo.mail_from} domain:${todo.domain}`)
325
316
  }
326
317
  process.exit()
327
318
  })()
@@ -379,12 +370,14 @@ if (parsed.version) {
379
370
  console.log('')
380
371
  for (const hook of getHooks()) {
381
372
  if (!plugins.registered_hooks[hook]) continue
382
- console.log(sprintf("%'--80s", `Hook: ${hook} `))
383
- console.log(sprintf('%-35s %-35s %-4s %-3s', 'Plugin', 'Method', 'Prio', 'T/O'))
384
- console.log(sprintf("%'-80s", ''))
373
+ console.log(`Hook: ${hook} `.padEnd(80, '-'))
374
+ console.log(`${'Plugin'.padEnd(35)} ${'Method'.padEnd(35)} ${'Prio'.padEnd(4)} T/O`)
375
+ console.log('-'.repeat(80))
385
376
  for (let p = 0; p < plugins.registered_hooks[hook].length; p++) {
386
377
  const item = plugins.registered_hooks[hook][p]
387
- console.log(sprintf('%-35s %-35s %4d %3d', item.plugin, item.method, item.priority, item.timeout))
378
+ console.log(
379
+ `${item.plugin.padEnd(35)} ${item.method.padEnd(35)} ${String(item.priority).padStart(4)} ${String(item.timeout).padStart(3)}`,
380
+ )
388
381
  }
389
382
  console.log('')
390
383
  }
package/config/plugins CHANGED
@@ -12,7 +12,6 @@
12
12
  # status
13
13
  # process_title
14
14
  # syslog
15
- # watch
16
15
 
17
16
  # CONNECT
18
17
  # ----------
@@ -20,7 +19,6 @@
20
19
  # karma
21
20
  # relay
22
21
  # access
23
- # p0f
24
22
  # geoip
25
23
  # asn
26
24
  # fcrdns
@@ -49,7 +47,6 @@ mail_from.is_resolvable
49
47
  # At least one rcpt_to plugin is REQUIRED for inbound email.
50
48
  rcpt_to.in_host_list
51
49
  # qmail-deliverable
52
- # rcpt_to.routes
53
50
 
54
51
  # DATA
55
52
  # ----------
@@ -19,3 +19,13 @@ port=2555
19
19
  ; should outbound messages be delivered by smtp_forward?
20
20
  ; see #1472 and #2795
21
21
  ; enable_outbound=false
22
+
23
+ ; Options here override the same option in tls.ini [main]
24
+ [tls]
25
+ ; rejectUnauthorized=true
26
+ ; minVersion=TLSv1.2
27
+ ; ciphers=ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256
28
+ ; key=outbound_tls_key.pem
29
+ ; cert=outbound_tls_cert.pem
30
+ ; no_tls_hosts[]=10.0.0.5
31
+ ; force_tls_hosts[]=mx.example.com
@@ -15,3 +15,13 @@ port=2555
15
15
  ; should outbound messages be delivered by smtp_proxy?
16
16
  ; see https://github.com/haraka/Haraka/issues/1472
17
17
  ; enable_outbound=true
18
+
19
+ ; Options here override the same option in tls.ini [main]
20
+ [tls]
21
+ ; rejectUnauthorized=true
22
+ ; minVersion=TLSv1.2
23
+ ; ciphers=ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256
24
+ ; key=outbound_tls_key.pem
25
+ ; cert=outbound_tls_cert.pem
26
+ ; no_tls_hosts[]=10.0.0.5
27
+ ; force_tls_hosts[]=mx.example.com
package/connection.js CHANGED
@@ -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
  }
@@ -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
  }
@@ -1,66 +1,153 @@
1
1
  # Connection Object
2
2
 
3
- For each connection to Haraka there is one connection object.
3
+ For each connection to Haraka there is one connection object. It is the first argument passed to almost every plugin hook and is the primary context object plugins use to inspect and act on the SMTP session.
4
4
 
5
- ## API
5
+ ## Properties
6
6
 
7
- - connection.uuid
7
+ ### connection.uuid
8
8
 
9
- A unique UUID for this connection.
9
+ A unique UUID for this connection. Used as the connection identifier in logs and inherited by `transaction.uuid`.
10
10
 
11
- - connection.remote - info about the host that is connecting to Haraka.
11
+ ### connection.remote
12
12
 
13
- - ip - remote IP address
14
- - host - reverse DNS of the remote hosts IP
15
- - is_private - true if the remote IP is from a private (loopback, RFC 1918, link local, etc.) IP address.
16
- - is_local - true if the remote IP is localhost (loopback, link local)
13
+ Information about the host connecting to Haraka.
17
14
 
18
- - connection.local - info about the host that is running Haraka
15
+ - `ip` remote IP address
16
+ - `port` — remote TCP port
17
+ - `host` — reverse DNS of the remote IP (populated by the `connect.rdns_access` / `connect` hooks)
18
+ - `info` — free-form descriptor (e.g. populated by FCrDNS)
19
+ - `closed` — `true` once the remote end has dropped the connection
20
+ - `is_private` — `true` if the remote IP is in a private range (RFC 1918, loopback, link-local, etc.)
21
+ - `is_local` — `true` if the remote IP is localhost / loopback
19
22
 
20
- - ip - the IP of the Haraka server, as reported by the OS
21
- - port - the port number handling the connection.
22
- - host - the rDNS host name of the local IP
23
+ ### connection.local
23
24
 
24
- - connection.proxy - proxy properties set when a proxy is used (like haproxy)
25
+ Information about the Haraka server endpoint handling this connection.
25
26
 
26
- - allowed - if the remote IP has proxy permission
27
- - ip - when proxied, the proxy servers IP address
28
- - type - currently null or 'haproxy'
27
+ - `ip` the IP of the Haraka server, as reported by the OS
28
+ - `port` the port number handling the connection
29
+ - `host` the primary host name of the Haraka server
30
+ - `info` — `Haraka` (with `/<version>` appended when `headers.show_version` is enabled in `connection.ini`)
29
31
 
30
- - connection.hello
32
+ ### connection.hello
31
33
 
32
- - verb - Either 'EHLO' or 'HELO' whichever the remote end used
33
- - host - The hostname given with HELO or EHLO
34
+ The greeting given by the client.
34
35
 
35
- - connection.notes
36
+ - `verb` — `EHLO` or `HELO`, whichever the client used
37
+ - `host` — the hostname argument
36
38
 
37
- An object which persists during the lifetime of the connection. It is used to store connection-specific properties. See also, connection.results and [haraka-notes](https://github.com/haraka/haraka-notes).
39
+ ### connection.tls
38
40
 
39
- - connection.transaction
41
+ State of the TLS layer on this connection.
40
42
 
41
- The current transaction object, valid after MAIL FROM, and destroyed at queue
42
- time, RSET time, or if MAIL FROM was rejected. See the Transaction Object
43
- documentation file.
43
+ - `enabled` `true` once STARTTLS has been negotiated (or the listener is `smtps`)
44
+ - `advertised` `true` if Haraka advertised STARTTLS in the EHLO response
45
+ - `verified` — `true` if the peer certificate validated against the configured CAs
46
+ - `cipher` — the negotiated cipher object (`name`, `version`, …)
47
+ - `verifyError` — the verification error, if any
48
+ - `peerCertificate` — the parsed peer certificate (when client certs are used)
44
49
 
45
- - connection.relaying
50
+ ### connection.proxy
46
51
 
47
- A boolean flag to say whether this connection is allowed to relay mails (i.e.
48
- deliver mails outbound). This is normally set by SMTP AUTH, or sometimes via
49
- an IP address check.
52
+ Proxy-protocol state, set when the connection arrived via HAProxy (see [HAProxy.md](HAProxy.md)).
50
53
 
51
- - connection.current_line
54
+ - `allowed` — `true` if the remote IP is in the `haproxy.hosts` allow-list
55
+ - `ip` — the proxy server's IP (the real client IP appears in `connection.remote.ip` once PROXY is parsed)
56
+ - `type` — currently `null` or `'haproxy'`
52
57
 
53
- For low level use. Contains the current line sent from the remote end,
54
- verbatim as it was sent. Can be useful in certain botnet detection techniques.
58
+ ### connection.notes
55
59
 
56
- - connection.last_response
60
+ A plain object that persists for the lifetime of the connection. Use it to share state between plugin hooks. For structured per-test results prefer `connection.results`. See also [haraka-notes](https://github.com/haraka/haraka-notes).
57
61
 
58
- Contains the last SMTP response sent to the client.
62
+ ### connection.results
59
63
 
60
- - connection.remote_closed
64
+ Structured store for plugin results. See [haraka-results](https://github.com/haraka/haraka-results).
61
65
 
62
- For low level use. This value is set when the remote host drops the connection.
66
+ ### connection.transaction
63
67
 
64
- - connection.results
68
+ The current `Transaction` object. Valid between `MAIL FROM` and the end of `queue` / `RSET` (or until `MAIL FROM` is rejected). See [Transaction.md](Transaction.md).
65
69
 
66
- Store results of processing in a structured format. See [haraka-results](https://github.com/haraka/haraka-results)
70
+ ### connection.relaying
71
+
72
+ Boolean. `true` if this connection is allowed to relay (i.e. deliver mail outbound). Normally set by an auth plugin or an IP allow-list. Reading or writing this property transparently routes through the current transaction when one exists, so the flag survives across multiple messages in a single connection only when set on the connection.
73
+
74
+ ### connection.capabilities
75
+
76
+ Array of ESMTP capabilities advertised in the EHLO response (e.g. `['PIPELINING', '8BITMIME', 'SIZE 0', 'STARTTLS', 'AUTH PLAIN LOGIN']`). Plugins may push additional capability strings during the `capabilities` hook.
77
+
78
+ ### connection.esmtp
79
+
80
+ `true` if the client used `EHLO` (as opposed to `HELO`).
81
+
82
+ ### connection.pipelining
83
+
84
+ `true` once Haraka has advertised, and the client has used, SMTP pipelining.
85
+
86
+ ### connection.early_talker
87
+
88
+ `true` if the client sent data before Haraka issued its banner — a
89
+ common spam-bot signal.
90
+
91
+ ### connection.tran_count
92
+
93
+ Number of transactions completed on this connection.
94
+
95
+ ### connection.rcpt_count / connection.msg_count
96
+
97
+ Per-disposition counters (`accept`, `tempfail`, `reject`) tracking
98
+ recipients and full messages on this connection.
99
+
100
+ ### connection.start_time
101
+
102
+ Connection start time, in epoch milliseconds (`Date.now()`).
103
+
104
+ ### connection.last_response
105
+
106
+ The last SMTP response line Haraka sent to the client.
107
+
108
+ ### connection.last_reject
109
+
110
+ The text of the last rejection issued to this client (used by
111
+ `max_unrecognized_commands` and similar throttling plugins).
112
+
113
+ ### connection.errors
114
+
115
+ Count of protocol errors on this connection.
116
+
117
+ ### connection.current_line
118
+
119
+ Low-level. The current line as sent by the remote end, verbatim. Useful
120
+ for botnet fingerprinting.
121
+
122
+ ### connection.state
123
+
124
+ The connection's protocol state — one of the values in `haraka-constants`'s `connection.state` table (`PAUSE`, `CMD`, `LOOP`, `DATA`, `DISCONNECTING`, `DISCONNECTED`).
125
+
126
+ ## Methods
127
+
128
+ ### connection.respond(code, msg, cb)
129
+
130
+ Send an SMTP response to the client. `code` is the numeric SMTP code, `msg` is the human-readable text (a string or an array of strings for a multi-line response). The callback fires when the response has been written.
131
+
132
+ ### connection.disconnect()
133
+
134
+ Close the connection after running the `disconnect` hook.
135
+
136
+ ### connection.reset_transaction(cb)
137
+
138
+ Tear down the current transaction (equivalent to `RSET`) and invoke `cb` when complete.
139
+
140
+ ### connection.set(path, value)
141
+
142
+ Assign a nested property safely, e.g. `connection.set('remote.host', 'mx.example.com')`. Setting `remote.ip`
143
+ automatically recomputes `remote.is_private` / `remote.is_local`.
144
+
145
+ ### connection.get(path)
146
+
147
+ Read a nested property, returning `undefined` if any segment is missing.
148
+
149
+ ### connection.loginfo / lognotice / logwarn / logerror / logdebug / logcrit / logalert / logemerg / logprotocol / logdata
150
+
151
+ Log at the named level. Each takes either `(msg)` or `(plugin, msg, data)`.
152
+
153
+ See [Logging.md](Logging.md).
@@ -1,76 +1,94 @@
1
1
  # Core Configuration Files
2
2
 
3
- See [Logging](Logging.md).
4
-
5
- The Haraka core reads some configuration files to determine a few actions:
6
-
7
- - smtp.yaml or smtp.json
8
-
9
- If either of these files exist then they are loaded first.
10
- This file is designed to use the JSON/YAML file overrides documented in
11
- [haraka-config](https://github.com/haraka/haraka-config) to optionally provide the entire configuration in a single file.
12
-
13
- - plugins
14
-
15
- The list of plugins to load
16
-
17
- - smtp.ini
18
-
19
- Keys:
20
-
21
- - listen_host, port - the host and port to listen on (default: ::0 and 25)
22
- - listen - (default: [::0]:25) Comma separated IP:Port addresses to listen on
23
- - inactivity_time - how long to let clients idle in seconds (default: 300)
24
- - nodes - specifies how many processes to fork. The string "cpus" will fork as many children as there are CPUs (default: 1, which enables cluster mode with a single process)
25
- - user - optionally a user to drop privileges to. Can be a string or UID.
26
- - group - optionally a group to drop privileges to. Can be a string or GID.
27
- - ignore_bad_plugins - If a plugin fails to compile by default Haraka will stop at load time.
28
- If, however, you wish to continue on without that plugin's facilities, then
29
- set this config option
30
- - daemonize - enable this to cause Haraka to fork into the background on start-up (default: 0)
31
- - daemon_log_file - (default: /var/log/haraka.log) where to redirect stdout/stderr when daemonized
32
- - daemon_pid_file - (default: /var/run/haraka.pid) where to write a PID file to
33
- - graceful_shutdown - (default: false) enable this to wait for sockets on shutdown instead of closing them quickly
34
- - force_shutdown_timeout - (default: 30) number of seconds to wait for a graceful shutdown
35
-
36
- - me
37
-
38
- A name to use for this server. Used in received lines and elsewhere. Setup
39
- by default to be your hostname.
40
-
41
- - connection.ini
42
-
43
- See inline comments in connection.ini for the following settings:
44
-
45
- - main.spool_dir
46
- - main.spool_after
47
- - haproxy.hosts_ipv4
48
- - haproxy.hosts_ipv6
49
- - headers.\*
50
- - max.bytes
51
- - max.line_length
52
- - max.data_line_length
53
- - max.mime_parts
54
- - message.greeting
55
- - message.close
56
- - smtputf8
57
- - strict_rfc1869
58
- - uuid.deny_chars
59
- - uuid.banner_bytes
60
-
61
- - plugin_timeout
62
-
63
- Seconds to allow a plugin to run before the next hook is called automatically
64
- default: 30
65
-
66
- Note also that each plugin can have a `config/<plugin_name>.timeout`
67
- file specifying a per-plugin timeout. In this file you can set a timeout of 0 to mean that this plugin's hooks never time out. Use this with care.
68
-
69
- If the plugin is in a sub-directory of plugins, then you must create this file
70
- in the equivalent path e.g. the queue/smtp_forward would need a timeout file in `config/queue/smtp_forward.timeout`
71
-
72
- - outbound.ini
73
-
74
- - outbound.bounce_message
75
-
76
- The bounce message if delivery of the message fails. The default is normally fine. Bounce messages contain a number of template replacement values which are best discovered by looking at the source code.
3
+ Haraka reads the configuration files in this document directly. Plugins typically own their own files (named after the plugin); see the individual plugin docs.
4
+
5
+ See also [Logging](Logging.md), [HAProxy](HAProxy.md), and [Outbound](Outbound.md).
6
+
7
+ ## smtp.yaml / smtp.json
8
+
9
+ If either of these files exists it is loaded first. It uses the YAML/JSON override mechanism from [haraka-config](https://github.com/haraka/haraka-config) and can provide the entire configuration as a single file.
10
+
11
+ ## plugins
12
+
13
+ A newline-delimited list of plugins to load. Lines starting with `#` are ignored.
14
+
15
+ ## smtp.ini
16
+
17
+ Controls the SMTP listener and the master process.
18
+
19
+ | Key | Default | Description |
20
+ | --- | --- | --- |
21
+ | `listen` | `[::0]:25` | Comma-separated `IP:port` pairs to listen on (e.g. `127.0.0.1:25,127.0.0.1:587`). |
22
+ | `listen_host` / `port` | | Legacy. If set, a `<listen_host>:<port>` entry is prepended to `listen`. Prefer `listen`. |
23
+ | `smtps_port` | `465` | Port used by the optional implicit-TLS listener. |
24
+ | `public_ip` | none | The server's public IP. Helps NAT-aware plugins (SPF, GeoIP) when Haraka is behind NAT. If `stun` is on `$PATH` Haraka will try to discover it automatically. |
25
+ | `inactivity_timeout` | `300` | Idle seconds before a client socket is dropped. |
26
+ | `nodes` | `1` | Number of worker processes to fork. The string `cpus` forks one per CPU. |
27
+ | `user` / `group` | | User and group to drop privileges to (name or numeric ID). |
28
+ | `ignore_bad_plugins` | `0` | If `1`, Haraka starts even if some plugins fail to compile. |
29
+ | `daemonize` | `false` | If `true`, fork into the background at start-up. |
30
+ | `daemon_log_file` | `/var/log/haraka.log` | Where to redirect stdout/stderr when daemonized. |
31
+ | `daemon_pid_file` | `/var/run/haraka.pid` | Where to write the PID file. |
32
+ | `graceful_shutdown` | `false` | If `true`, wait for in-flight sockets on shutdown. |
33
+ | `force_shutdown_timeout` | `30` | Seconds to wait before forcing shutdown. |
34
+
35
+ ## me
36
+
37
+ A single-line file containing the server name used in `Received:` headers
38
+ and elsewhere. Defaults to `hostname(1)`.
39
+
40
+ ## connection.ini
41
+
42
+ Per-connection limits and behaviours. See inline comments in the shipped
43
+ `config/connection.ini` for full details.
44
+
45
+ | Section / Key | Default | Description |
46
+ | --- | --- | --- |
47
+ | `main.spool_dir` | `/tmp` | Directory for temporary spool files. |
48
+ | `main.spool_after` | `-1` | Size (bytes) at which to spool the message to disk. `-1` never spools; `0` always spools. |
49
+ | `main.strict_rfc1869` | `false` | Reject `MAIL FROM` / `RCPT TO` that violates RFC 1869/821 (spurious spaces, missing brackets). |
50
+ | `main.smtputf8` | `true` | Advertise `SMTPUTF8` (RFC 6531). |
51
+ | `haproxy.hosts` | empty | Array of IPs/CIDRs allowed to send the PROXY protocol. See [HAProxy.md](HAProxy.md). |
52
+ | `headers.add_received` | `true` | Add a `Received:` header to incoming mail. |
53
+ | `headers.clean_auth_results` | `true` | Strip inbound `Authentication-Results:` headers before plugins run. |
54
+ | `headers.show_version` | `true` | Include the Haraka version in the SMTP banner and the `Received:` header. |
55
+ | `headers.max_lines` | `1000` | Maximum number of header lines accepted. |
56
+ | `headers.max_received` | `100` | Maximum number of `Received:` headers before mail is rejected as looping. |
57
+ | `max.bytes` | `26214400` | Maximum message size (advertised as the `SIZE` ESMTP extension). |
58
+ | `max.line_length` | `512` | SMTP command line length cap. Clients exceeding this are dropped with `521`. |
59
+ | `max.data_line_length` | `992` | Maximum line length in `DATA`. Longer lines are wrapped with `CRLF SPACE` (Sendmail behaviour) and `transaction.notes.data_line_length_exceeded` is set. |
60
+ | `max.mime_parts` | `1000` | Maximum MIME parts per message. |
61
+ | `message.greeting` | — | Array. Lines used as the SMTP greeting banner. |
62
+ | `message.helo` | `Haraka is at your service.` | Reply text for `HELO` / `EHLO`. |
63
+ | `message.close` | `closing connection. …` | Reply text on `QUIT`. |
64
+ | `uuid.banner_chars` | `6` | Number of UUID chars included in the SMTP banner (`0` to disable, `40` for the full UUID). |
65
+ | `uuid.deny_chars` | `0` | Number of UUID chars prepended (in brackets) to deny messages. |
66
+
67
+ ## plugin_timeout
68
+
69
+ Single-integer file. Seconds to allow a plugin hook to run before Haraka
70
+ automatically advances to the next hook. Default: `30`.
71
+
72
+ A per-plugin override may be placed at `config/<plugin>.timeout`. A value
73
+ of `0` disables the timeout for that plugin — use with care. Plugins in
74
+ subdirectories need a matching path: `queue/smtp_forward` looks for
75
+ `config/queue/smtp_forward.timeout`.
76
+
77
+ ## outbound.ini
78
+
79
+ Configures the outbound delivery engine. The most common keys are listed
80
+ below; see [Outbound.md](Outbound.md) for the full set.
81
+
82
+ | Key | Default | Description |
83
+ | --- | --- | --- |
84
+ | `disabled` | `false` | Disable outbound delivery entirely. |
85
+ | `concurrency_max` | `100` | Maximum simultaneous outbound deliveries. |
86
+ | `enable_tls` | `true` | Use STARTTLS opportunistically on outbound deliveries. |
87
+ | `maxTempFailures` | `13` | Number of temporary failures before a message bounces. |
88
+ | `always_split` | `false` | Create one queue file per recipient instead of one per destination domain. |
89
+ | `received_header` | `Haraka outbound` | Value used in the outbound `Received:` header. |
90
+ | `inet_prefer` | `default` | IP family preference for MX lookups: `v4`, `v6`, or `default` (OS preference). |
91
+
92
+ ## outbound.bounce_message
93
+
94
+ Template used when an outbound message bounces. The default is usually fine. Available template variables are documented in the source of `outbound/hmail.js`.
package/docs/HAProxy.md CHANGED
@@ -1,43 +1,59 @@
1
- # HAProxy PROXY protocol extension support
1
+ # HAProxy PROXY Protocol Support
2
2
 
3
- Haraka supports PROXY protocol [1].
3
+ Haraka supports version 1 (text format) of the HAProxy [PROXY protocol][proxy-spec], which allows an upstream proxy to tell Haraka the real client IP and port. DNSBLs, allow-lists, and other IP-based plugins then see the original client rather than the proxy.
4
4
 
5
- This allows an upstream proxy to pass the IP address and port of the remote client. Haraka will use the remote IP instead of the socket IP address (which is the proxy). This allows DNSBLs and access control lists to use the correct source address.
5
+ The PROXY v2 binary header is not currently supported.
6
6
 
7
- Support is disabled by default. Attempts to send a PROXY command will return a DENYSOFTDISCONNECT error. DENYSOFT is used to prevent configuration errors from rejecting valid mail.
7
+ ## Configuration
8
8
 
9
- To enable support for PROXY you must populate connection.ini[haproxy]hosts[] with the IP addresses of the HAProxy hosts that MUST send the PROXY command. Ranges can be specified with CIDR notation.
9
+ PROXY support is disabled by default. To enable it, list the IPs (or CIDRs) of trusted proxies in `connection.ini`:
10
10
 
11
- When a proxy host connects to Haraka, a banner is not sent. Instead Haraka awaits the PROXY command. The connection will timeout with `421 PROXY timed out` if the command is not sent within 30 seconds.
11
+ ```ini
12
+ [haproxy]
13
+ hosts[] = 192.0.2.4
14
+ hosts[] = 192.0.2.5/30
15
+ hosts[] = 2001:db8::1
16
+ ```
17
+
18
+ Connections from any other IP get a `DENYSOFTDISCONNECT` if they send a `PROXY` command. `DENYSOFT` is deliberate — it avoids permanently rejecting valid mail when a misconfiguration causes a legitimate proxy to fall outside the allow-list.
19
+
20
+ When a listed proxy connects, Haraka **does not** send the SMTP banner; it waits for the `PROXY` command. If none arrives within 30 seconds the connection is closed with `421 PROXY timed out`.
12
21
 
13
- NOTE: because Haraka does not send a banner when a listed HAProxy host connects you must set check-send-proxy to ensure that the service checks send a PROXY command before they run.
22
+ ## What plugins see after PROXY is parsed
14
23
 
15
- [1] http://haproxy.1wt.eu/download/1.5/doc/proxy-protocol.txt
24
+ | Property | Value |
25
+ | --- | --- |
26
+ | `connection.remote.ip` / `.port` | the **real** client address from the PROXY header |
27
+ | `connection.local.ip` / `.port` | the destination IP/port from the PROXY header |
28
+ | `connection.proxy.allowed` | `true` |
29
+ | `connection.proxy.ip` | the proxy's address (i.e. the original socket peer) |
30
+ | `connection.proxy.type` | `'haproxy'` |
31
+ | `connection.notes.proxy` | full parsed record: `{ type, proto, src_ip, src_port, dst_ip, dst_port, proxy_ip }` |
16
32
 
17
- HAProxy supports the PROXY protocol in version 1.5 or later.
33
+ ## HAProxy configuration
18
34
 
19
- Here is an example listener section for haproxy.cfg:
35
+ You need HAProxy 1.5. The `send-proxy` option on each backend server tells HAProxy to emit the v1 header on every connection.
20
36
 
21
37
  ```
22
38
  listen smtp :25
23
- mode tcp
24
- option tcplog
25
- option smtpchk
26
- balance roundrobin
27
- server smtp1 ip.of.haraka.server1:25 check-send-proxy check inter 10s send-proxy
28
- server smtp2 ip.of.haraka.server2:25 check-send-proxy check inter 10s send-proxy
29
- server smtp3 ip.of.haraka.server3:25 check-send-proxy check inter 10s send-proxy
30
- server smtp4 ip.of.haraka.server4:25 check-send-proxy check inter 10s send-proxy
31
- server smtp5 ip.of.haraka.server5:25 check-send-proxy check inter 10s send-proxy
39
+ mode tcp
40
+ option tcplog
41
+ balance roundrobin
42
+ server smtp1 ip.of.haraka1:25 check-send-proxy check inter 10s send-proxy
43
+ server smtp2 ip.of.haraka2:25 check-send-proxy check inter 10s send-proxy
32
44
  ```
33
45
 
34
- The important part is `send-proxy` which causes HAProxy to send the PROXY extension on connection.
46
+ The `check-send-proxy` flag is required for HAProxy's health checks because Haraka does not respond with a banner before the PROXY header arrives.
35
47
 
36
- When using `option smtpchk` you will see CONNRESET errors reported in the Haraka logs as smtpchk drops the connection before the HELO response is still being written. You can use the `option tcp-check` instead to provide a better service check by having the check wait for the banner, send QUIT and then check the response:
48
+ ### Health checks
49
+
50
+ `option smtpchk` drops the connection mid-handshake and shows up as `CONNRESET` in Haraka's logs. A cleaner check is to use `tcp-check` and politely close with `QUIT`:
37
51
 
38
52
  ```
39
- option tcp-check
40
- tcp-check expect rstring ^220\
41
- tcp-check send QUIT\r\n
42
- tcp-check expect rstring ^221\
53
+ option tcp-check
54
+ tcp-check expect rstring ^220
55
+ tcp-check send QUIT\r\n
56
+ tcp-check expect rstring ^221
43
57
  ```
58
+
59
+ [proxy-spec]: https://www.haproxy.org/download/2.8/doc/proxy-protocol.txt