Haraka 3.1.4 → 3.1.6

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 (55) hide show
  1. package/.prettierignore +1 -1
  2. package/{Changes.md → CHANGELOG.md} +34 -0
  3. package/CONTRIBUTORS.md +26 -26
  4. package/README.md +68 -93
  5. package/SECURITY.md +178 -0
  6. package/bin/haraka +7 -14
  7. package/config/plugins +0 -3
  8. package/docs/Connection.md +126 -39
  9. package/docs/CoreConfig.md +92 -74
  10. package/docs/HAProxy.md +41 -25
  11. package/docs/Logging.md +68 -38
  12. package/docs/Outbound.md +124 -179
  13. package/docs/Plugins.md +38 -59
  14. package/docs/Transaction.md +78 -83
  15. package/docs/Tutorial.md +122 -209
  16. package/docs/plugins/aliases.md +1 -141
  17. package/docs/plugins/auth/auth_ldap.md +2 -39
  18. package/docs/plugins/max_unrecognized_commands.md +4 -18
  19. package/docs/plugins/process_title.md +3 -3
  20. package/docs/plugins/reseed_rng.md +11 -13
  21. package/docs/plugins/tls.md +7 -7
  22. package/docs/plugins/toobusy.md +10 -4
  23. package/docs/tutorials/SettingUpOutbound.md +40 -48
  24. package/endpoint.js +32 -2
  25. package/outbound/hmail.js +3 -2
  26. package/outbound/index.js +3 -0
  27. package/package.json +21 -34
  28. package/plugins/queue/smtp_forward.js +4 -4
  29. package/run_tests +3 -15
  30. package/server.js +17 -7
  31. package/smtp_client.js +8 -6
  32. package/test/connection.js +234 -0
  33. package/test/endpoint.js +32 -4
  34. package/test/host_pool.js +57 -31
  35. package/test/logger.js +75 -135
  36. package/test/outbound/bounce_net_errors.js +87 -131
  37. package/test/outbound/bounce_rfc3464.js +177 -254
  38. package/test/outbound/hmail.js +19 -0
  39. package/test/outbound/index.js +189 -0
  40. package/test/outbound/queue.js +92 -0
  41. package/test/plugins/auth/auth_base.js +39 -44
  42. package/test/plugins/auth/auth_vpopmaild.js +8 -9
  43. package/test/plugins/queue/smtp_forward.js +953 -183
  44. package/test/plugins/rcpt_to.host_list_base.js +58 -93
  45. package/test/plugins/rcpt_to.in_host_list.js +126 -175
  46. package/test/plugins/record_envelope_addresses.js +8 -8
  47. package/test/plugins/status.js +10 -10
  48. package/test/plugins/tls.js +9 -19
  49. package/test/plugins/xclient.js +75 -110
  50. package/test/plugins.js +10 -13
  51. package/test/rfc1869.js +50 -70
  52. package/test/server.js +438 -421
  53. package/test/smtp_client.js +1192 -218
  54. package/test/tls_socket.js +242 -0
  55. package/tls_socket.js +18 -22
@@ -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
package/docs/Logging.md CHANGED
@@ -1,83 +1,109 @@
1
1
  # Haraka Logging
2
2
 
3
- Haraka has built-in logging (see API docs below) and support for log plugins.
3
+ Haraka has a built-in logger (described below) and a plugin hook (`log`) that lets log plugins ship messages elsewhere — for example to syslog via [haraka-plugin-syslog](https://github.com/haraka/haraka-plugin-syslog).
4
4
 
5
- - log.ini
5
+ ## Configuration
6
6
 
7
- Contains settings for log level, timestamps, and format. See the example log.ini file for examples.
7
+ ### log.ini
8
8
 
9
- - loglevel
9
+ ```ini
10
+ [main]
11
+ ; data, protocol, debug, info, notice, warn, error, crit, alert, emerg
12
+ level=info
10
13
 
11
- The loglevel file provides a finger-friendly way to change the loglevel on the CLI. Use it like so: `echo DEBUG > config/loglevel`. When the level in log.ini is set and the loglevel file is present, the loglevel file wins. During runtime, whichever was edited most recently wins.
14
+ ; prepend ISO-8601 timestamps to log entries (built-in logger only)
15
+ timestamps=false
12
16
 
13
- ## Logging API
17
+ ; default, logfmt, json
18
+ format=default
19
+ ```
14
20
 
15
- Logging conventions within Haraka
21
+ ### loglevel
16
22
 
17
- This section pertains to the built in logging. For log plugins like ([haraka-plugin-syslog](https://github.com/haraka/haraka-plugin-syslog)), refer to the plugin's docs.
23
+ A single-line file for quick CLI tweaks:
18
24
 
19
- The logline by default will be in the form of:
25
+ ```sh
26
+ echo DEBUG > config/loglevel
27
+ ```
20
28
 
21
- [level] [uuid] [origin] message
29
+ When both `log.ini` and the `loglevel` file are present, whichever was edited most recently wins at runtime — `loglevel` is convenient for an interactive bump without touching `log.ini`.
22
30
 
23
- Where origin is "core" or the name of the plugin which triggered the message, and "uuid" is the ID of the connection associated with the message.
31
+ ### log_timestamps
24
32
 
25
- When calling a log method on logger, you should provide the plugin object and the connection object anywhere in the arguments
26
- to the log method.
33
+ A single-value file that toggles timestamp prepending. Equivalent to `main.timestamps` in `log.ini`. If either source enables timestamps, they are enabled.
27
34
 
28
- logger.logdebug("i like turtles", plugin, connection);
35
+ ## Log Levels
29
36
 
30
- Will yield, for example,
37
+ In ascending severity (and decreasing verbosity):
31
38
 
32
- [DEBUG] [7F1C820F-DC79-4192-9AA6-5307354B20A6] [dnsbl] i like turtles
39
+ | Level | Numeric | Use |
40
+ | -------- | ------- | --- |
41
+ | DATA | 9 | message body bytes — extremely verbose |
42
+ | PROTOCOL | 8 | SMTP wire protocol |
43
+ | DEBUG | 7 | developer diagnostics |
44
+ | INFO | 6 | general informational |
45
+ | NOTICE | 5 | normal but significant events (connect/disconnect, summary lines) |
46
+ | WARN | 4 | recoverable problems |
47
+ | ERROR | 3 | non-fatal errors |
48
+ | CRIT | 2 | critical errors |
49
+ | ALERT | 1 | needs immediate attention |
50
+ | EMERG | 0 | unusable |
33
51
 
34
- If you call the log method on the connection object, you can forego the connection as argument:
52
+ A message is emitted when its level the configured level.
35
53
 
36
- connection.logdebug("turtles all the way down", plugin);
54
+ ## Logging API
37
55
 
38
- and similarly for the log methods on the plugin object:
56
+ Every log call ultimately produces:
39
57
 
40
- plugin.logdebug("he just really likes turtles", connection);
58
+ [level] [uuid] [origin] message
41
59
 
42
- failing to provide a connection and/or plugin object will leavethe default values in the log (currently "core").
60
+ `origin` is `core` or the plugin name; `uuid` is the connection UUID (with `.N` appended for the Nth transaction).
43
61
 
44
- This is implemented by testing for argument type in the logger.js log\* method. objects-as-arguments are then sniffed to try to determine if they're a connection or plugin instance.
62
+ The simplest call is on the connection or plugin object origin and uuid are filled in automatically:
45
63
 
46
- ### Log formats
64
+ ```js
65
+ connection.logdebug('turtles all the way down')
66
+ plugin.loginfo('checking sender', connection)
67
+ ```
47
68
 
48
- Apart from the default log format described above, Haraka also supports logging as [`logfmt`](https://brandur.org/logfmt) and JSON. These can be used by changing the `format` attribute in `log.ini` to the desired format, e.g.:
69
+ Each of the level names has a matching method:
70
+ `logdata`, `logprotocol`, `logdebug`, `loginfo`, `lognotice`, `logwarn`,
71
+ `logerror`, `logcrit`, `logalert`, `logemerg`.
49
72
 
50
- ```ini
51
- ; format=default
52
- ; format=json
53
- format=logfmt
73
+ Calling the logger directly works too — pass the plugin and/or connection anywhere in the arguments and the logger sniffs them:
74
+
75
+ ```js
76
+ logger.logdebug('i like turtles', plugin, connection)
77
+ // → [DEBUG] [7F1C820F-…] [dnsbl] i like turtles
54
78
  ```
55
79
 
56
- Here's an example of a log line generated with `logfmt`:
80
+ Plain objects mixed into the arguments are merged into the log record (in `logfmt` / `json` formats) or stringified (`key=value` pairs in the default format).
81
+
82
+ ## Log Formats
83
+
84
+ Set `main.format` in `log.ini` to one of `default`, `logfmt`, or `json`.
85
+
86
+ `logfmt`:
57
87
 
58
- level=PROTOCOL uuid=9FF7F70E-5D57-435A-AAD9-EA069B6159D9.1 source=core message="S: 354 go ahead, make my day"
88
+ level=PROTOCOL uuid=9FF7F70E-…1 source=core message="S: 354 go ahead, make my day"
59
89
 
60
- And the same line formatted as JSON:
90
+ `json`:
61
91
 
62
92
  ```json
63
93
  {
64
94
  "level": "PROTOCOL",
65
- "uuid": "9FF7F70E-5D57-435A-AAD9-EA069B6159D9.1",
95
+ "uuid": "9FF7F70E-…1",
66
96
  "source": "core",
67
97
  "message": "S: 354 go ahead, make my day"
68
98
  }
69
99
  ```
70
100
 
71
- Any objects passed to the log methods will also have their properties included in the log line. For example, using `logfmt`:
72
-
73
- level=NOTICE uuid=9FF7F70E-5D57-435A-AAD9-EA069B6159D9.1 source=core message=disconnect ip=127.0.0.1 rdns=Unknown helo=3h2dnz8a0if relay=N early=N esmtp=N tls=N pipe=N errors=0 txns=1 rcpts=1/0/0 msgs=1/0/0 bytes=222 lr="" time=0.052
74
-
75
- And using JSON:
101
+ A typical structured disconnect line looks like:
76
102
 
77
103
  ```json
78
104
  {
79
105
  "level": "NOTICE",
80
- "uuid": "9FF7F70E-5D57-435A-AAD9-EA069B6159D9.1",
106
+ "uuid": "9FF7F70E-…1",
81
107
  "source": "core",
82
108
  "message": "disconnect",
83
109
  "ip": "127.0.0.1",
@@ -97,3 +123,7 @@ And using JSON:
97
123
  "time": 0.052
98
124
  }
99
125
  ```
126
+
127
+ ## The `log` hook
128
+
129
+ Each log message becomes a `log` hook invocation. The built-in handler writes to stdout (with ANSI colour when stdout is a TTY); log plugins can return `OK` or `STOP` to suppress the built-in output and ship the message elsewhere. Messages emitted before plugins finish loading are buffered and replayed once the plugin chain is ready.